##// END OF EJS Templates
Merged 1.1 into default branch.
neko259 -
r146:60af707d merge default
parent child Browse files
Show More
@@ -0,0 +1,130 b''
1 # -*- coding: utf-8 -*-
2 import 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 # Deleting model 'Admin'
12 db.delete_table(u'boards_admin')
13
14 # Adding model 'User'
15 db.create_table(u'boards_user', (
16 (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
17 ('user_id', self.gf('django.db.models.fields.CharField')(max_length=50)),
18 ('rank', self.gf('django.db.models.fields.IntegerField')()),
19 ('registration_time', self.gf('django.db.models.fields.DateTimeField')()),
20 ('last_access_time', self.gf('django.db.models.fields.DateTimeField')()),
21 ))
22 db.send_create_signal(u'boards', ['User'])
23
24 # Adding M2M table for field fav_tags on 'User'
25 m2m_table_name = db.shorten_name(u'boards_user_fav_tags')
26 db.create_table(m2m_table_name, (
27 ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
28 ('user', models.ForeignKey(orm[u'boards.user'], null=False)),
29 ('tag', models.ForeignKey(orm[u'boards.tag'], null=False))
30 ))
31 db.create_unique(m2m_table_name, ['user_id', 'tag_id'])
32
33 # Adding M2M table for field fav_threads on 'User'
34 m2m_table_name = db.shorten_name(u'boards_user_fav_threads')
35 db.create_table(m2m_table_name, (
36 ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
37 ('user', models.ForeignKey(orm[u'boards.user'], null=False)),
38 ('post', models.ForeignKey(orm[u'boards.post'], null=False))
39 ))
40 db.create_unique(m2m_table_name, ['user_id', 'post_id'])
41
42 # Adding model 'Setting'
43 db.create_table(u'boards_setting', (
44 (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
45 ('name', self.gf('django.db.models.fields.CharField')(max_length=50)),
46 ('value', self.gf('django.db.models.fields.CharField')(max_length=50)),
47 ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['boards.User'])),
48 ))
49 db.send_create_signal(u'boards', ['Setting'])
50
51 # Adding field 'Post.user'
52 db.add_column(u'boards_post', 'user',
53 self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['boards.User'], null=True),
54 keep_default=False)
55
56
57 def backwards(self, orm):
58 # Adding model 'Admin'
59 db.create_table(u'boards_admin', (
60 ('password', self.gf('django.db.models.fields.CharField')(max_length=100)),
61 (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
62 ('name', self.gf('django.db.models.fields.CharField')(max_length=100)),
63 ))
64 db.send_create_signal(u'boards', ['Admin'])
65
66 # Deleting model 'User'
67 db.delete_table(u'boards_user')
68
69 # Removing M2M table for field fav_tags on 'User'
70 db.delete_table(db.shorten_name(u'boards_user_fav_tags'))
71
72 # Removing M2M table for field fav_threads on 'User'
73 db.delete_table(db.shorten_name(u'boards_user_fav_threads'))
74
75 # Deleting model 'Setting'
76 db.delete_table(u'boards_setting')
77
78 # Deleting field 'Post.user'
79 db.delete_column(u'boards_post', 'user_id')
80
81
82 models = {
83 u'boards.ban': {
84 'Meta': {'object_name': 'Ban'},
85 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
86 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'})
87 },
88 u'boards.post': {
89 'Meta': {'object_name': 'Post'},
90 '_text_rendered': ('django.db.models.fields.TextField', [], {}),
91 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
92 'image': ('boards.thumbs.ImageWithThumbsField', [], {'max_length': '100', 'blank': 'True'}),
93 'image_height': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
94 'image_width': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
95 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}),
96 'parent': ('django.db.models.fields.BigIntegerField', [], {}),
97 'poster_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
98 'poster_user_agent': ('django.db.models.fields.TextField', [], {}),
99 'pub_time': ('django.db.models.fields.DateTimeField', [], {}),
100 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['boards.Tag']", 'symmetrical': 'False'}),
101 'text': ('markupfield.fields.MarkupField', [], {'rendered_field': 'True'}),
102 'text_markup_type': ('django.db.models.fields.CharField', [], {'default': "'markdown'", 'max_length': '30'}),
103 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
104 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['boards.User']", 'null': 'True'})
105 },
106 u'boards.setting': {
107 'Meta': {'object_name': 'Setting'},
108 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
109 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
110 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['boards.User']"}),
111 'value': ('django.db.models.fields.CharField', [], {'max_length': '50'})
112 },
113 u'boards.tag': {
114 'Meta': {'object_name': 'Tag'},
115 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
116 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
117 },
118 u'boards.user': {
119 'Meta': {'object_name': 'User'},
120 'fav_tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['boards.Tag']", 'symmetrical': 'False'}),
121 'fav_threads': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'+'", 'symmetrical': 'False', 'to': u"orm['boards.Post']"}),
122 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
123 'last_access_time': ('django.db.models.fields.DateTimeField', [], {}),
124 'rank': ('django.db.models.fields.IntegerField', [], {}),
125 'registration_time': ('django.db.models.fields.DateTimeField', [], {}),
126 'user_id': ('django.db.models.fields.CharField', [], {'max_length': '50'})
127 }
128 }
129
130 complete_apps = ['boards'] No newline at end of file
@@ -1,7 +1,7 b''
1 1 from django.contrib import admin
2 from boards.models import Post, Tag, Admin, Ban
2 from boards.models import Post, Tag, User, Ban
3 3
4 4 admin.site.register(Post)
5 5 admin.site.register(Tag)
6 admin.site.register(Admin)
7 admin.site.register(Ban) No newline at end of file
6 admin.site.register(User)
7 admin.site.register(Ban)
@@ -1,134 +1,151 b''
1 1 import re
2 2 from captcha.fields import CaptchaField
3 3 from django import forms
4 4 from django.forms.util import ErrorList
5 from boards.models import TITLE_MAX_LENGTH
5 from boards.models import TITLE_MAX_LENGTH, User
6 6 from neboard import settings
7 7 from boards import utils
8 8
9 9 class PlainErrorList(ErrorList):
10 10 def __unicode__(self):
11 11 return self.as_text()
12 12
13 13 def as_text(self):
14 14 return ''.join([u'(!) %s ' % e for e in self])
15 15
16 16
17 17 class PostForm(forms.Form):
18 18
19 19 MAX_TEXT_LENGTH = 10000
20 20 MAX_IMAGE_SIZE = 8 * 1024 * 1024
21 21
22 22 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False)
23 23 text = forms.CharField(widget=forms.Textarea, required=False)
24 24 image = forms.ImageField(required=False)
25 25
26 26 def clean_title(self):
27 27 title = self.cleaned_data['title']
28 28 if title:
29 29 if len(title) > TITLE_MAX_LENGTH:
30 30 raise forms.ValidationError('Title must have less than' +
31 31 str(TITLE_MAX_LENGTH) +
32 32 ' characters.')
33 33 return title
34 34
35 35 def clean_text(self):
36 36 text = self.cleaned_data['text']
37 37 if text:
38 38 if len(text) > self.MAX_TEXT_LENGTH:
39 39 raise forms.ValidationError('Text must have less than ' +
40 40 str(self.MAX_TEXT_LENGTH) +
41 41 ' characters.')
42 42 return text
43 43
44 44 def clean_image(self):
45 45 image = self.cleaned_data['image']
46 46 if image:
47 47 if image._size > self.MAX_IMAGE_SIZE:
48 48 raise forms.ValidationError('Image must be less than ' +
49 49 str(self.MAX_IMAGE_SIZE) +
50 50 ' bytes.')
51 51 return image
52 52
53 53 def clean(self):
54 54 cleaned_data = super(PostForm, self).clean()
55 55
56 56 self._clean_text_image()
57 57
58 58 return cleaned_data
59 59
60 60 def _clean_text_image(self):
61 61 text = self.cleaned_data.get('text')
62 62 image = self.cleaned_data.get('image')
63 63
64 64 if (not text) and (not image):
65 65 error_message = 'Either text or image must be entered.'
66 66 self._errors['text'] = self.error_class([error_message])
67 67 self._errors['image'] = self.error_class([error_message])
68 68
69 69
70
71 70 class ThreadForm(PostForm):
72 71 regex_tags = re.compile(ur'^[\w\s\d]+$', re.UNICODE)
73 72 tags = forms.CharField(max_length=100)
74 73
75 74 def clean_tags(self):
76 75 tags = self.cleaned_data['tags']
77 76
78 77 if tags:
79 78 if not self.regex_tags.match(tags):
80 79 raise forms.ValidationError(
81 80 'Inappropriate characters in tags.')
82 81
83 82 return tags
84 83
85 84 def clean(self):
86 85 cleaned_data = super(ThreadForm, self).clean()
87 86
88 87 return cleaned_data
89 88
90 89
91 90 class PostCaptchaForm(PostForm):
92 91 captcha = CaptchaField()
93 92
94 93 def __init__(self, *args, **kwargs):
95 94 self.request = kwargs['request']
96 95 del kwargs['request']
97 96
98 97 super(PostCaptchaForm, self).__init__(*args, **kwargs)
99 98
100 99 def clean(self):
101 100 cleaned_data = super(PostCaptchaForm, self).clean()
102 101
103 102 success = self.is_valid()
104 103 utils.update_captcha_access(self.request, success)
105 104
106 105 if success:
107 106 return cleaned_data
108 107 else:
109 108 raise forms.ValidationError("captcha validation failed")
110 109
111 110
112 111 class ThreadCaptchaForm(ThreadForm):
113 112 captcha = CaptchaField()
114 113
115 114 def __init__(self, *args, **kwargs):
116 115 self.request = kwargs['request']
117 116 del kwargs['request']
118 117
119 118 super(ThreadCaptchaForm, self).__init__(*args, **kwargs)
120 119
121 120 def clean(self):
122 121 cleaned_data = super(ThreadCaptchaForm, self).clean()
123 122
124 123 success = self.is_valid()
125 124 utils.update_captcha_access(self.request, success)
126 125
127 126 if success:
128 127 return cleaned_data
129 128 else:
130 129 raise forms.ValidationError("captcha validation failed")
131 130
132 131
133 132 class SettingsForm(forms.Form):
134 133 theme = forms.ChoiceField(choices=settings.THEMES, widget=forms.RadioSelect)
134
135
136 class LoginForm(forms.Form):
137 user_id = forms.CharField()
138
139 def clean_user_id(self):
140 user_id = self.cleaned_data['user_id']
141 if user_id:
142 users = User.objects.filter(user_id=user_id)
143 if len(users) == 0:
144 raise forms.ValidationError('No such user found')
145
146 return user_id
147
148 def clean(self):
149 cleaned_data = super(LoginForm, self).clean()
150
151 return cleaned_data No newline at end of file
1 NO CONTENT: modified file, binary diff hidden
@@ -1,189 +1,233 b''
1 1 # SOME DESCRIPTIVE TITLE.
2 2 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3 3 # This file is distributed under the same license as the PACKAGE package.
4 4 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
5 5 #
6 6 msgid ""
7 7 msgstr ""
8 8 "Project-Id-Version: PACKAGE VERSION\n"
9 9 "Report-Msgid-Bugs-To: \n"
10 "POT-Creation-Date: 2013-08-30 18:54+0300\n"
10 "POT-Creation-Date: 2013-09-07 19:43+0300\n"
11 11 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
12 12 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
13 13 "Language-Team: LANGUAGE <LL@li.org>\n"
14 14 "Language: ru\n"
15 15 "MIME-Version: 1.0\n"
16 16 "Content-Type: text/plain; charset=UTF-8\n"
17 17 "Content-Transfer-Encoding: 8bit\n"
18 18 "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
19 19 "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
20 20
21 21 #: templates/boards/404.html:6
22 22 msgid "Not found"
23 23 msgstr "НС найдСно"
24 24
25 25 #: templates/boards/404.html:12
26 26 msgid "This page does not exist"
27 27 msgstr "Π­Ρ‚ΠΎΠΉ страницы Π½Π΅ сущСствуСт"
28 28
29 29 #: templates/boards/authors.html:6
30 30 msgid "Authors"
31 31 msgstr "Авторы"
32 32
33 33 #: templates/boards/authors.html:24
34 34 msgid "Distributed under the"
35 35 msgstr "РаспространяСтся ΠΏΠΎΠ΄"
36 36
37 37 #: templates/boards/authors.html:26
38 38 msgid "license"
39 39 msgstr "Π»ΠΈΡ†Π΅Π½Π·ΠΈΠ΅ΠΉ"
40 40
41 41 #: templates/boards/authors.html:28
42 42 msgid "Repository"
43 43 msgstr "Π Π΅ΠΏΠΎΠ·ΠΈΡ‚ΠΎΡ€ΠΈΠΉ"
44 44
45 45 #: templates/boards/banned.html:6
46 46 msgid "Banned"
47 47 msgstr "Π—Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Π½"
48 48
49 49 #: templates/boards/banned.html:11
50 50 msgid "Your IP address has been banned. Contact the administrator"
51 51 msgstr "Π’Π°Ρˆ IP адрСс Π±Ρ‹Π» Π·Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Π½. Π‘Π²ΡΠΆΠΈΡ‚Π΅ΡΡŒ с администратором"
52 52
53 53 #: templates/boards/base.html:12
54 54 msgid "Feed"
55 55 msgstr "Π›Π΅Π½Ρ‚Π°"
56 56
57 #: templates/boards/base.html:36
57 #: templates/boards/base.html:29
58 58 msgid "All threads"
59 59 msgstr "ВсС Ρ‚Π΅ΠΌΡ‹"
60 60
61 #: templates/boards/base.html:42
61 #: templates/boards/base.html:35
62 62 msgid "Settings"
63 63 msgstr "Настройки"
64 64
65 #: templates/boards/base.html:50
65 #: templates/boards/base.html:42 templates/boards/login.html:6
66 #: templates/boards/login.html.py:21
67 msgid "Login"
68 msgstr "Π’Ρ…ΠΎΠ΄"
69
70 #: templates/boards/base.html:43
66 71 msgid "Up"
67 72 msgstr "Π’Π²Π΅Ρ€Ρ…"
68 73
74 #: templates/boards/login.html:15
75 msgid "User ID"
76 msgstr "ID ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ"
77
78 #: templates/boards/login.html:24
79 msgid "Insert your user id above"
80 msgstr "Π’ΡΡ‚Π°Π²ΡŒΡ‚Π΅ свой ID ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ Π²Ρ‹ΡˆΠ΅"
81
69 82 #: templates/boards/posting_general.html:18
70 83 msgid "Tag: "
71 84 msgstr "Π’Π΅Π³: "
72 85
73 86 #: templates/boards/posting_general.html:35
74 #: templates/boards/posting_general.html:81 templates/boards/thread.html:27
87 #: templates/boards/posting_general.html:89 templates/boards/thread.html:27
75 88 #: templates/boards/rss/post.html:5
76 89 msgid "Post image"
77 90 msgstr "Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ сообщСния"
78 91
79 92 #: templates/boards/posting_general.html:48
80 93 msgid "Reply"
81 94 msgstr "ΠžΡ‚Π²Π΅Ρ‚"
82 95
83 #: templates/boards/posting_general.html:55 templates/boards/thread.html:108
96 #: templates/boards/posting_general.html:54 templates/boards/thread.html:46
97 msgid "Delete"
98 msgstr "Π£Π΄Π°Π»ΠΈΡ‚ΡŒ"
99
100 #: templates/boards/posting_general.html:63 templates/boards/thread.html:113
84 101 msgid "replies"
85 102 msgstr "ΠΎΡ‚Π²Π΅Ρ‚ΠΎΠ²"
86 103
87 #: templates/boards/posting_general.html:56 templates/boards/thread.html:109
104 #: templates/boards/posting_general.html:64 templates/boards/thread.html:114
88 105 msgid "images"
89 106 msgstr "ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ"
90 107
91 #: templates/boards/posting_general.html:58
92 #: templates/boards/posting_general.html:131 templates/boards/thread.html:48
93 #: templates/boards/rss/post.html:10
108 #: templates/boards/posting_general.html:66
109 #: templates/boards/posting_general.html:139 templates/boards/tags.html:7
110 #: templates/boards/thread.html:56 templates/boards/rss/post.html:10
94 111 msgid "Tags"
95 112 msgstr "Π’Π΅Π³ΠΈ"
96 113
97 #: templates/boards/posting_general.html:113
114 #: templates/boards/posting_general.html:115
115 msgid "No threads exist. Create the first one!"
116 msgstr "НСт Ρ‚Π΅ΠΌ. Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ ΠΏΠ΅Ρ€Π²ΡƒΡŽ!"
117
118 #: templates/boards/posting_general.html:121
98 119 msgid "Create new thread"
99 120 msgstr "Π‘ΠΎΠ·Π΄Π°Ρ‚ΡŒ Π½ΠΎΠ²ΡƒΡŽ Ρ‚Π΅ΠΌΡƒ"
100 121
101 #: templates/boards/posting_general.html:116 templates/boards/thread.html:70
122 #: templates/boards/posting_general.html:124 templates/boards/thread.html:75
102 123 msgid "Title"
103 124 msgstr "Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ"
104 125
105 #: templates/boards/posting_general.html:121 templates/boards/thread.html:75
126 #: templates/boards/posting_general.html:129 templates/boards/thread.html:80
106 127 msgid "Text"
107 128 msgstr "ВСкст"
108 129
109 #: templates/boards/posting_general.html:126 templates/boards/thread.html:80
130 #: templates/boards/posting_general.html:134 templates/boards/thread.html:85
110 131 msgid "Image"
111 132 msgstr "Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅"
112 133
113 #: templates/boards/posting_general.html:141 templates/boards/thread.html:91
134 #: templates/boards/posting_general.html:149 templates/boards/thread.html:96
114 135 msgid "Post"
115 136 msgstr "ΠžΡ‚ΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ"
116 137
117 #: templates/boards/posting_general.html:143
138 #: templates/boards/posting_general.html:151
118 139 msgid "Tags must be delimited by spaces. Text or image is required."
119 140 msgstr ""
120 141 "Π’Π΅Π³ΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ Ρ€Π°Π·Π΄Π΅Π»Π΅Π½Ρ‹ ΠΏΡ€ΠΎΠ±Π΅Π»Π°ΠΌΠΈ. ВСкст ΠΈΠ»ΠΈ ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹."
121 142
122 #: templates/boards/posting_general.html:146 templates/boards/thread.html:93
143 #: templates/boards/posting_general.html:154 templates/boards/thread.html:98
123 144 msgid "Basic markdown syntax."
124 145 msgstr "Π‘Π°Π·ΠΎΠ²Ρ‹ΠΉ синтаксис markdown."
125 146
126 #: templates/boards/posting_general.html:156
147 #: templates/boards/posting_general.html:164
127 148 msgid "Pages:"
128 149 msgstr "Π‘Ρ‚Ρ€Π°Π½ΠΈΡ†Ρ‹: "
129 150
130 #: templates/boards/settings.html:13
151 #: templates/boards/settings.html:12
152 msgid "User:"
153 msgstr "ΠŸΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ:"
154
155 #: templates/boards/settings.html:14
156 msgid "You are moderator."
157 msgstr "Π’Ρ‹ ΠΌΠΎΠ΄Π΅Ρ€Π°Ρ‚ΠΎΡ€."
158
159 #: templates/boards/settings.html:20
131 160 msgid "Theme"
132 161 msgstr "Π’Π΅ΠΌΠ°"
133 162
134 #: templates/boards/settings.html:29
163 #: templates/boards/settings.html:36
135 164 msgid "Save"
136 165 msgstr "Π‘ΠΎΡ…Ρ€Π°Π½ΠΈΡ‚ΡŒ"
137 166
138 #: templates/boards/tags.html:7
139 msgid "tags"
140 msgstr "Ρ‚Π΅Π³ΠΎΠ²"
167 #: templates/boards/tags.html:17
168 msgid "threads"
169 msgstr "Ρ‚Π΅ΠΌ"
170
171 #: templates/boards/tags.html:20
172 msgid "Remove"
173 msgstr "Π£Π΄Π°Π»ΠΈΡ‚ΡŒ"
141 174
142 #: templates/boards/thread.html:67
175 #: templates/boards/tags.html:23
176 msgid "Add"
177 msgstr "Π”ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ"
178
179 #: templates/boards/tags.html:28
180 msgid "No tags found."
181 msgstr "Π’Π΅Π³ΠΈ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½Ρ‹."
182
183 #: templates/boards/thread.html:72
143 184 msgid "Reply to thread"
144 185 msgstr "ΠžΡ‚Π²Π΅Ρ‚ΠΈΡ‚ΡŒ Π² Ρ‚Π΅ΠΌΡƒ"
145 186
146 #: templates/boards/thread.html:94
187 #: templates/boards/thread.html:99
147 188 msgid "Example: "
148 189 msgstr "ΠŸΡ€ΠΈΠΌΠ΅Ρ€: "
149 190
150 #: templates/boards/thread.html:94
191 #: templates/boards/thread.html:99
151 192 msgid "italic"
152 193 msgstr "курсив"
153 194
154 #: templates/boards/thread.html:95
195 #: templates/boards/thread.html:100
155 196 msgid "bold"
156 197 msgstr "ΠΏΠΎΠ»ΡƒΠΆΠΈΡ€Π½Ρ‹ΠΉ"
157 198
158 #: templates/boards/thread.html:96
199 #: templates/boards/thread.html:101
159 200 msgid "Quotes can be inserted with"
160 201 msgstr "Π¦ΠΈΡ‚Π°Ρ‚Ρ‹ ΠΌΠΎΠ³ΡƒΡ‚ Π±Ρ‹Ρ‚ΡŒ вставлСны ΠΏΡ€ΠΈ ΠΏΠΎΠΌΠΎΡ‰ΠΈ"
161 202
162 #: templates/boards/thread.html:97
203 #: templates/boards/thread.html:102
163 204 msgid "Links to answers can be inserted with"
164 205 msgstr "Бсылки Π½Π° ΠΎΡ‚Π²Π΅Ρ‚Ρ‹ ΠΌΠΎΠ³ΡƒΡ‚ Π±Ρ‹Ρ‚ΡŒ вставлСны с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ"
165 206
166 #: templates/boards/thread.html:110
207 #: templates/boards/thread.html:115
167 208 msgid "Last update: "
168 209 msgstr "ПослСднСС обновлСниС: "
169 210
211 #~ msgid "tags"
212 #~ msgstr "Ρ‚Π΅Π³ΠΎΠ²"
213
170 214 #~ msgid "Get!"
171 215 #~ msgstr "Π“Π΅Ρ‚!"
172 216
173 217 #~ msgid "View"
174 218 #~ msgstr "ΠŸΡ€ΠΎΡΠΌΠΎΡ‚Ρ€"
175 219
176 220 #~ msgid "gets"
177 221 #~ msgstr "Π³Π΅Ρ‚ΠΎΠ²"
178 222
179 223 #~ msgid "author"
180 224 #~ msgstr "Π°Π²Ρ‚ΠΎΡ€"
181 225
182 226 #~ msgid "developer"
183 227 #~ msgstr "Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ"
184 228
185 229 #~ msgid "javascript developer"
186 230 #~ msgstr "Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ javascript"
187 231
188 232 #~ msgid "designer"
189 233 #~ msgstr "Π΄ΠΈΠ·Π°ΠΉΠ½Π΅Ρ€"
@@ -1,290 +1,331 b''
1 1 import os
2 2 from random import random
3 3 import re
4 4 import time
5 5 import math
6 6
7 7 from django.db import models
8 8 from django.http import Http404
9 9 from django.utils import timezone
10 10 from markupfield.fields import MarkupField
11 11 from threading import Thread
12 12
13 13 from neboard import settings
14 14 import thumbs
15 15
16 16 IMAGE_THUMB_SIZE = (200, 150)
17 17
18 18 TITLE_MAX_LENGTH = 50
19 19
20 20 DEFAULT_MARKUP_TYPE = 'markdown'
21 21
22 22 NO_PARENT = -1
23 23 NO_IP = '0.0.0.0'
24 24 UNKNOWN_UA = ''
25 25 ALL_PAGES = -1
26 26 OPENING_POST_POPULARITY_WEIGHT = 2
27 27 IMAGES_DIRECTORY = 'images/'
28 28 FILE_EXTENSION_DELIMITER = '.'
29 29
30 RANK_ADMIN = 0
31 RANK_MODERATOR = 10
32 RANK_USER = 100
33
30 34
31 35 class PostManager(models.Manager):
32 36 def create_post(self, title, text, image=None, parent_id=NO_PARENT,
33 ip=NO_IP, tags=None):
37 ip=NO_IP, tags=None, user=None):
34 38 post = self.create(title=title,
35 39 text=text,
36 40 pub_time=timezone.now(),
37 41 parent=parent_id,
38 42 image=image,
39 43 poster_ip=ip,
40 44 poster_user_agent=UNKNOWN_UA,
41 last_edit_time=timezone.now())
45 last_edit_time=timezone.now(),
46 user=user)
42 47
43 48 if tags:
44 49 map(post.tags.add, tags)
45 50
46 51 if parent_id != NO_PARENT:
47 52 self._bump_thread(parent_id)
48 53 else:
49 54 self._delete_old_threads()
50 55
51 56 return post
52 57
53 58 def delete_post(self, post):
54 59 children = self.filter(parent=post.id)
55 60 for child in children:
56 61 self.delete_post(child)
57 62 post.delete()
58 63
59 64 def delete_posts_by_ip(self, ip):
60 65 posts = self.filter(poster_ip=ip)
61 66 for post in posts:
62 67 self.delete_post(post)
63 68
64 69 def get_threads(self, tag=None, page=ALL_PAGES,
65 70 order_by='-last_edit_time'):
66 71 if tag:
67 72 threads = self.filter(parent=NO_PARENT, tags=tag)
68 73
69 74 # TODO Throw error 404 if no threads for tag found?
70 75 else:
71 76 threads = self.filter(parent=NO_PARENT)
72 77
73 78 threads = threads.order_by(order_by)
74 79
75 80 if page != ALL_PAGES:
76 81 thread_count = len(threads)
77 82
78 83 if page < self.get_thread_page_count(tag=tag):
79 84 start_thread = page * settings.THREADS_PER_PAGE
80 85 end_thread = min(start_thread + settings.THREADS_PER_PAGE,
81 86 thread_count)
82 87 threads = threads[start_thread:end_thread]
83 88
84 89 return threads
85 90
86 91 def get_thread(self, opening_post_id):
87 92 try:
88 93 opening_post = self.get(id=opening_post_id, parent=NO_PARENT)
89 94 except Post.DoesNotExist:
90 95 raise Http404
91 96
92 97 if opening_post.parent == NO_PARENT:
93 98 replies = self.filter(parent=opening_post_id)
94 99
95 100 thread = [opening_post]
96 101 thread.extend(replies)
97 102
98 103 return thread
99 104
100 105 def exists(self, post_id):
101 106 posts = self.filter(id=post_id)
102 107
103 108 return posts.count() > 0
104 109
105 110 def get_thread_page_count(self, tag=None):
106 111 if tag:
107 112 threads = self.filter(parent=NO_PARENT, tags=tag)
108 113 else:
109 114 threads = self.filter(parent=NO_PARENT)
110 115
111 116 return int(math.ceil(threads.count() / float(
112 117 settings.THREADS_PER_PAGE)))
113 118
114 119 def _delete_old_threads(self):
115 120 """
116 121 Preserves maximum thread count. If there are too many threads,
117 122 delete the old ones.
118 123 """
119 124
120 125 # TODO Move old threads to the archive instead of deleting them.
121 126 # Maybe make some 'old' field in the model to indicate the thread
122 127 # must not be shown and be able for replying.
123 128
124 129 threads = self.get_threads()
125 130 thread_count = len(threads)
126 131
127 132 if thread_count > settings.MAX_THREAD_COUNT:
128 133 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
129 134 old_threads = threads[thread_count - num_threads_to_delete:]
130 135
131 136 for thread in old_threads:
132 137 self.delete_post(thread)
133 138
134 139 def _bump_thread(self, thread_id):
135 140 thread = self.get(id=thread_id)
136 141
137 142 if thread.can_bump():
138 143 thread.last_edit_time = timezone.now()
139 144 thread.save()
140 145
141 146
142 147 class TagManager(models.Manager):
143 148 def get_not_empty_tags(self):
144 149 all_tags = self.all().order_by('name')
145 150 tags = []
146 151 for tag in all_tags:
147 152 if not tag.is_empty():
148 153 tags.append(tag)
149 154
150 155 return tags
151 156
152 157 def get_popular_tags(self):
153 158 all_tags = self.get_not_empty_tags()
154 159
155 160 sorted_tags = sorted(all_tags, key=lambda tag: tag.get_popularity(),
156 161 reverse=True)
157 162
158 163 return sorted_tags[:settings.POPULAR_TAGS]
159 164
160 165
161 166 class Tag(models.Model):
162 167 """
163 168 A tag is a text node assigned to the post. The tag serves as a board
164 169 section. There can be multiple tags for each message
165 170 """
166 171
167 172 objects = TagManager()
168 173
169 174 name = models.CharField(max_length=100)
170 175 # TODO Connect the tag to its posts to check the number of threads for
171 176 # the tag.
172 177
173 178 def __unicode__(self):
174 179 return self.name
175 180
176 181 def is_empty(self):
177 182 return self.get_post_count() == 0
178 183
179 184 def get_post_count(self):
180 185 posts_with_tag = Post.objects.get_threads(tag=self)
181 186 return posts_with_tag.count()
182 187
183 188 def get_popularity(self):
184 189 posts_with_tag = Post.objects.get_threads(tag=self)
185 190 reply_count = 0
186 191 for post in posts_with_tag:
187 192 reply_count += post.get_reply_count()
188 193 reply_count += OPENING_POST_POPULARITY_WEIGHT
189 194
190 195 return reply_count
191 196
192 197
193 198 class Post(models.Model):
194 199 """A post is a message."""
195 200
196 201 objects = PostManager()
197 202
198 203 def _update_image_filename(self, filename):
199 204 """Get unique image filename"""
200 205
201 206 path = IMAGES_DIRECTORY
202 207 new_name = str(int(time.mktime(time.gmtime())))
203 208 new_name += str(int(random() * 1000))
204 209 new_name += FILE_EXTENSION_DELIMITER
205 210 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
206 211
207 212 return os.path.join(path, new_name)
208 213
209 214 title = models.CharField(max_length=TITLE_MAX_LENGTH)
210 215 pub_time = models.DateTimeField()
211 216 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
212 217 escape_html=False)
213 218
214 219 image_width = models.IntegerField(default=0)
215 220 image_height = models.IntegerField(default=0)
216 221
217 222 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
218 223 blank=True, sizes=(IMAGE_THUMB_SIZE,),
219 224 width_field='image_width',
220 225 height_field='image_height')
221 226
222 227 poster_ip = models.GenericIPAddressField()
223 228 poster_user_agent = models.TextField()
224 229 parent = models.BigIntegerField()
225 230 tags = models.ManyToManyField(Tag)
226 231 last_edit_time = models.DateTimeField()
232 user = models.ForeignKey('User', null=True, default=None)
227 233
228 234 def __unicode__(self):
229 235 return '#' + str(self.id) + ' ' + self.title + ' (' + \
230 236 self.text.raw[:50] + ')'
231 237
232 238 def _get_replies(self):
233 239 return Post.objects.filter(parent=self.id)
234 240
235 241 def get_reply_count(self):
236 242 return self._get_replies().count()
237 243
238 244 def get_images_count(self):
239 245 images_count = 1 if self.image else 0
240 246 for reply in self._get_replies():
241 247 if reply.image:
242 248 images_count += 1
243 249
244 250 return images_count
245 251
246 252 def get_gets_count(self):
247 253 gets_count = 1 if self.is_get() else 0
248 254 for reply in self._get_replies():
249 255 if reply.is_get():
250 256 gets_count += 1
251 257
252 258 return gets_count
253 259
254 260 def can_bump(self):
255 261 """Check if the thread can be bumped by replying"""
256 262
257 263 replies_count = len(Post.objects.get_thread(self.id))
258 264
259 265 return replies_count <= settings.MAX_POSTS_PER_THREAD
260 266
261 267 def get_last_replies(self):
262 268 if settings.LAST_REPLIES_COUNT > 0:
263 269 reply_count = self.get_reply_count()
264 270
265 271 if reply_count > 0:
266 272 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
267 273 reply_count)
268 274 last_replies = self._get_replies()[reply_count
269 275 - reply_count_to_show:]
270 276
271 277 return last_replies
272 278
273 279
274 class Admin(models.Model):
275 """
276 Model for admin users
277 """
278 name = models.CharField(max_length=100)
279 password = models.CharField(max_length=100)
280 class User(models.Model):
281
282 user_id = models.CharField(max_length=50)
283 rank = models.IntegerField()
284
285 registration_time = models.DateTimeField()
286 last_access_time = models.DateTimeField()
287
288 fav_tags = models.ManyToManyField(Tag, null=True, blank=True)
289 fav_threads = models.ManyToManyField(Post, related_name='+', null=True,
290 blank=True)
291
292 def save_setting(self, name, value):
293 setting, created = Setting.objects.get_or_create(name=name, user=self)
294 setting.value = value
295 setting.save()
296
297 return setting
298
299 def get_setting(self, name):
300 settings = Setting.objects.filter(name=name, user=self)
301 if len(settings) > 0:
302 setting = settings[0]
303 else:
304 setting = None
305
306 if setting:
307 setting_value = setting.value
308 else:
309 setting_value = None
310
311 return setting_value
312
313 def is_moderator(self):
314 return RANK_MODERATOR >= self.rank
280 315
281 316 def __unicode__(self):
282 return self.name + '/' + '*' * len(self.password)
317 return self.user_id
318
319
320 class Setting(models.Model):
321
322 name = models.CharField(max_length=50)
323 value = models.CharField(max_length=50)
324 user = models.ForeignKey(User)
283 325
284 326
285 327 class Ban(models.Model):
286
287 328 ip = models.GenericIPAddressField()
288 329
289 330 def __unicode__(self):
290 331 return self.ip
@@ -1,285 +1,290 b''
1 1 html {
2 2 background: #555;
3 3 color: #ffffff;
4 4 }
5 5
6 6 #admin_panel {
7 7 background: #FF0000;
8 8 color: #00FF00
9 9 }
10 10
11 11 .input_field {
12 12
13 13 }
14 14
15 15 .input_field_name {
16 16
17 17 }
18 18
19 19 .input_field_error {
20 20 color: #FF0000;
21 21 }
22 22
23 23
24 24 .title {
25 25 font-weight: bold;
26 26 color: #ffcc00;
27 27 }
28 28
29 29 .link, a {
30 30 color: #afdcec;
31 31 }
32 32
33 33 .block {
34 34 display: inline-block;
35 35 vertical-align: top;
36 36 }
37 37
38 38 .tag {
39 39 color: #b4cfec;
40 40 }
41 41
42 42 .post_id {
43 43 color: #fff380;
44 44 }
45 45
46 46 .post, .dead_post {
47 47 background: #333;
48 48 margin: 5px;
49 49 padding: 10px;
50 50 border: solid 1px #888;
51 51 clear: left;
52 52 word-wrap: break-word;
53 53 }
54 54
55 55 .metadata {
56 56 padding: 5px;
57 57 margin-top: 10px;
58 58 border: solid 1px #666;
59 59 font-size: 0.9em;
60 60 color: #ddd;
61 61 display: table;
62 62 }
63 63
64 64 .navigation_panel, .tag_info {
65 65 background: #444;
66 66 margin: 5px;
67 67 padding: 10px;
68 68 border: solid 1px #888;
69 69 color: #eee;
70 70 }
71 71
72 72 .navigation_panel .link {
73 73 border-right: 1px solid #fff;
74 74 font-weight: bold;
75 75 margin-right: 1ex;
76 76 padding-right: 1ex;
77 77 }
78 78 .navigation_panel .link:last-child {
79 79 border-left: 1px solid #fff;
80 80 border-right: none;
81 81 float: right;
82 82 margin-left: 1ex;
83 83 margin-right: 0;
84 84 padding-left: 1ex;
85 85 padding-right: 0;
86 86 }
87 87
88 88 .navigation_panel::after, .post::after {
89 89 clear: both;
90 90 content: ".";
91 91 display: block;
92 92 height: 0;
93 93 line-height: 0;
94 94 visibility: hidden;
95 95 }
96 96
97 97 p {
98 98 margin-top: .5em;
99 99 margin-bottom: .5em;
100 100 }
101 101
102 102 .post-form-w {
103 103 display: table;
104 104 background: #333344;
105 105 border: solid 1px #888;
106 106 color: #fff;
107 107 padding: 10px;
108 108 margin: 5px
109 109 }
110 110
111 111 .form-row {
112 112 display: table-row;
113 113 }
114 114
115 115 .form-label, .form-input, .form-errors {
116 116 display: table-cell;
117 117 }
118 118
119 119 .form-label {
120 120 padding: .25em 1ex .25em 0;
121 121 vertical-align: top;
122 122 }
123 123
124 124 .form-input {
125 125 padding: .25em 0;
126 126 }
127 127
128 128 .form-errors {
129 129 padding-left: 1ex;
130 130 font-weight: bold;
131 vertical-align: top;
131 vertical-align: middle;
132 132 }
133 133
134 134 .post-form input, .post-form textarea {
135 135 background: #333;
136 136 color: #fff;
137 137 border: solid 1px;
138 138 padding: 0;
139 139 width: 100%;
140 140 }
141 141
142 142 .form-submit {
143 143 border-bottom: 2px solid #ddd;
144 144 margin-bottom: .5em;
145 145 padding-bottom: .5em;
146 146 }
147 147
148 148 .form-title {
149 149 font-weight: bold;
150 150 }
151 151
152 152 input[type="submit"] {
153 153 background: #222;
154 154 border: solid 1px #fff;
155 155 color: #fff;
156 156 }
157 157
158 158 blockquote {
159 159 border-left: solid 2px;
160 160 padding-left: 5px;
161 161 color: #B1FB17;
162 162 margin: 0;
163 163 }
164 164
165 165 .post > .image {
166 166 float: left;
167 167 margin: 0 1ex .5ex 0;
168 168 min-width: 1px;
169 169 text-align: center;
170 170 display: table-row;
171 171 }
172 172
173 173 .post > .metadata {
174 174 clear: left;
175 175 }
176 176
177 177 .get {
178 178 font-weight: bold;
179 179 color: #d55;
180 180 }
181 181
182 182 * {
183 183 text-decoration: none;
184 184 }
185 185
186 186 .dead_post {
187 187 background-color: #442222;
188 188 }
189 189
190 190 .quote {
191 191 color: #92cf38;
192 192 font-style: italic;
193 193 }
194 194
195 195 .spoiler {
196 196 background: white;
197 197 color: white;
198 198 }
199 199
200 200 .spoiler:hover {
201 201 color: black;
202 202 }
203 203
204 204 .comment {
205 205 color: #eb2;
206 206 font-style: italic;
207 207 }
208 208
209 209 a:hover {
210 210 text-decoration: underline;
211 211 }
212 212
213 213 .last-replies {
214 214 margin-left: 3ex;
215 215 }
216 216
217 217 .thread {
218 218 margin-bottom: 3ex;
219 219 }
220 220
221 221 .post:target {
222 222 border: solid 2px white;
223 223 }
224 224
225 225 pre{
226 226 white-space:pre-wrap
227 227 }
228 228
229 229 li {
230 230 list-style-position: inside;
231 231 }
232 232
233 233 .fancybox-skin {
234 234 position: relative;
235 235 background-color: #fff;
236 236 color: #ddd;
237 237 text-shadow: none;
238 238 }
239 239
240 240 .fancybox-image {
241 241 border: 1px solid black;
242 242 }
243 243
244 244 .image-mode-tab {
245 245 background: #444;
246 246 color: #eee;
247 247 display: table;
248 248 margin: 5px;
249 249 padding: 5px;
250 250 border: 1px solid #888;
251 251 }
252 252
253 253 .image-mode-tab > label {
254 254 margin: 0 1ex;
255 255 }
256 256
257 257 .image-mode-tab > label > input {
258 258 margin-right: .5ex;
259 259 }
260 260
261 261 #posts-table {
262 262 margin: 5px;
263 263 }
264 264
265 265 .tag_info {
266 266 display: table;
267 267 }
268 268
269 269 .tag_info > h2 {
270 270 margin: 0;
271 271 }
272 272
273 273 .post-info {
274 274 color: #ddd;
275 275 }
276 276
277 .moderator_info {
278 color: #e99d41;
279 float: right;
280 }
281
277 282 .refmap {
278 283 font-size: 0.9em;
279 284 color: #ccc;
280 285 margin-top: 1em;
281 286 }
282 287
283 288 input[type="submit"]:hover {
284 289 background: #555;
285 290 }
@@ -1,270 +1,275 b''
1 1 * {
2 2 font-size: inherit;
3 3 margin: 0;
4 4 padding: 0;
5 5 }
6 6 html {
7 7 background: #fff;
8 8 color: #000;
9 9 font: medium sans-serif;
10 10 }
11 11 a {
12 12 color: inherit;
13 13 text-decoration: underline;
14 14 }
15 15 li {
16 16 list-style-position: inside;
17 17 }
18 18
19 19 #admin_panel {
20 20 background: #182F6F;
21 21 color: #fff;
22 22 padding: .5ex 1ex .5ex 1ex;
23 23 }
24 24
25 25 .navigation_panel {
26 26 background: #182F6F;
27 27 color: #B4CFEC;
28 28 margin-bottom: 1em;
29 29 padding: .5ex 1ex 1ex 1ex;
30 30 }
31 31 .navigation_panel::after {
32 32 clear: both;
33 33 content: ".";
34 34 display: block;
35 35 height: 0;
36 36 line-height: 0;
37 37 visibility: hidden;
38 38 }
39 39
40 40 .navigation_panel a:link, .navigation_panel a:visited, .navigation_panel a:hover {
41 41 text-decoration: none;
42 42 }
43 43
44 44 .navigation_panel .link {
45 45 border-right: 1px solid #fff;
46 46 color: #fff;
47 47 font-weight: bold;
48 48 margin-right: 1ex;
49 49 padding-right: 1ex;
50 50 }
51 51 .navigation_panel .link:last-child {
52 52 border-left: 1px solid #fff;
53 53 border-right: none;
54 54 float: right;
55 55 margin-left: 1ex;
56 56 margin-right: 0;
57 57 padding-left: 1ex;
58 58 padding-right: 0;
59 59 }
60 60
61 61 .navigation_panel .tag {
62 62 color: #fff;
63 63 }
64 64
65 65 .input_field {
66 66
67 67 }
68 68
69 69 .input_field_name {
70 70
71 71 }
72 72
73 73 .input_field_error {
74 74 color: #FF0000;
75 75 }
76 76
77 77
78 78 .title {
79 79 color: #182F6F;
80 80 font-weight: bold;
81 81 }
82 82
83 83 .post-form-w {
84 84 background: #182F6F;
85 85 border-radius: 1ex;
86 86 color: #fff;
87 87 margin: 1em 1ex;
88 88 padding: 1ex;
89 89 }
90 90 .post-form {
91 91 display: table;
92 92 border-collapse: collapse;
93 93 width: 100%;
94 94
95 95 }
96 96 .form-row {
97 97 display: table-row;
98 98 }
99 99 .form-label, .form-input {
100 100 display: table-cell;
101 101 vertical-align: top;
102 102 }
103 103 .form-label {
104 104 padding: .25em 1ex .25em 0;
105 105 }
106 106 .form-input {
107 107 padding: .25em 0;
108 108 }
109 109 .form-input > * {
110 110 background: #fff;
111 111 color: #000;
112 112 border: none;
113 113 padding: 0;
114 114 resize: vertical;
115 115 width: 100%;
116 116 }
117 117 .form-submit {
118 118 border-bottom: 1px solid #666;
119 119 margin-bottom: .5em;
120 120 padding-bottom: .5em;
121 121 }
122 122 .form-title {
123 123 font-weight: bold;
124 124 margin-bottom: .5em;
125 125 }
126 126 .post-form .settings_item {
127 127 margin: .5em 0;
128 128 }
129 129 .form-submit input {
130 130 margin-top: .5em;
131 131 padding: .2em 1ex;
132 132 }
133 133 .form-label {
134 134 text-align: right;
135 135 }
136 136
137 137 .block {
138 138 display: inline-block;
139 139 vertical-align: top;
140 140 }
141 141
142 142 .post_id {
143 143 color: #a00;
144 144 }
145 145
146 146 .post {
147 147 clear: left;
148 148 margin: 0 1ex 1em 1ex;
149 149 overflow-x: auto;
150 150 word-wrap: break-word;
151 151 }
152 152 .last-replies > .post, #posts > .post {
153 153 border-bottom: 1px solid #182F6F;
154 154 padding-bottom: 1em;
155 155 }
156 156 #posts > .post:last-child {
157 157 border-bottom: none;
158 158 padding-bottom: 0;
159 159 }
160 160
161 161 .metadata {
162 162 background: #C0E4E8;
163 163 border: 1px solid #7F9699;
164 164 border-radius: .4ex;
165 165 display: table;
166 166 margin-top: .5em;
167 167 padding: .4em;
168 168 }
169 169
170 170 .post ul, .post ol {
171 171 margin: .5em 0 .5em 3ex;
172 172 }
173 173 .post li {
174 174 margin: .2em 0;
175 175 }
176 176 .post p {
177 177 margin: .5em 0;
178 178 }
179 179 .post blockquote {
180 180 border-left: 3px solid #182F6F;
181 181 margin: .5em 0 .5em 3ex;
182 182 padding-left: 1ex;
183 183 }
184 184 .post blockquote > blockquote {
185 185 padding-top: .1em;
186 186 }
187 187
188 188 .post > .image {
189 189 float: left;
190 190 margin-right: 1ex;
191 191 }
192 192 .post > .metadata {
193 193 clear: left;
194 194 }
195 195
196 196 .post > .message .get {
197 197 color: #182F6F; font-weight: bold;
198 198 }
199 199
200 200 .dead_post > .metadata {
201 201 background: #eee;
202 202 }
203 203
204 204 .quote {
205 205 color: #182F6F;
206 206 }
207 207
208 208 .spoiler {
209 209 background: black;
210 210 color: black;
211 211 }
212 212
213 213 .spoiler:hover {
214 214 background: #ffffff;
215 215 }
216 216
217 217 .comment {
218 218 color: #557055;
219 219 }
220 220
221 221 .last-replies {
222 222 margin-left: 6ex;
223 223 }
224 224
225 225 .thread > .post > .message > .post-info {
226 226 border-bottom: 2px solid #182F6F;
227 227 padding-bottom: .5em;
228 228 }
229 229
230 230 .last-replies > .post:last-child {
231 231 border-bottom: none;
232 232 padding-bottom: 0;
233 233 }
234 234
235 235 :target .post_id {
236 236 background: #182F6F;
237 237 color: #FFF;
238 238 text-decoration: none;
239 239 }
240 240
241 241 .image-mode-tab {
242 242 background: #182F6F;
243 243 color: #FFF;
244 244 display: table;
245 245 margin: 1em auto 1em 0;
246 246 padding: .2em .5ex;
247 247 }
248 248
249 249 .image-mode-tab > label {
250 250 margin: 0 1ex;
251 251 }
252 252
253 253 .image-mode-tab > label > input {
254 254 margin-right: .5ex;
255 255 }
256 256
257 257 .tag_info {
258 258 margin: 1em 0;
259 259 text-align: center;
260 260 }
261 261
262 262 .form-errors {
263 263 margin-left: 1ex;
264 264 }
265 265
266 .moderator_info {
267 font-weight: bold;
268 float: right;
269 }
270
266 271 .refmap {
267 272 border: 1px dashed #aaa;
268 273 padding: 0.5em;
269 274 display: table;
270 } No newline at end of file
275 }
@@ -1,54 +1,47 b''
1 1 {% load staticfiles %}
2 2 {% load i18n %}
3 3
4 4 <!DOCTYPE html>
5 5 <html>
6 6 <head>
7 7 <link rel="stylesheet" type="text/css"
8 8 href="{{ STATIC_URL }}css/jquery.fancybox.css" media="all"/>
9 9 <link rel="stylesheet" type="text/css"
10 10 href="{{ STATIC_URL }}css/{{ theme }}/base_page.css" media="all"/>
11 11 <link rel="alternate" type="application/rss+xml" href="rss/" title="
12 12 {% trans 'Feed' %}"/>
13 13
14 14 <link rel="icon" type="image/png"
15 15 href="{{ STATIC_URL }}favicon.png">
16 16
17 17 <meta name="viewport" content="width=device-width, initial-scale=1"/>
18 18 <meta charset="utf-8"/>
19 19 {% block head %}{% endblock %}
20 20 </head>
21 21 <body>
22 22 <script src="{{ STATIC_URL }}js/jquery-2.0.1.min.js"></script>
23 23 <script src="{{ STATIC_URL }}js/jquery.fancybox.pack.js"></script>
24 24 <script src="{% url 'django.views.i18n.javascript_catalog' %}"></script>
25 25 <script src="{{ STATIC_URL }}js/refmaps.js"></script>
26 26 <script src="{{ STATIC_URL }}js/main.js"></script>
27 <div id="admin_panel">
28
29 {% if request.session.admin == True %}
30 Admin panel TODO: Need to implement <BR />
31 {% endif %}
32
33 </div>
34 27
35 28 <div class="navigation_panel">
36 29 <a class="link" href="{% url 'index' %}">{% trans "All threads" %}</a>
37 30 {% for tag in tags %}
38 <a class="tag" href=" {% url 'tag' tag_name=tag.name %}">
39 {{ tag.name }}</a>({{ tag.get_post_count }})
31 <a class="tag" href="{% url 'tag' tag_name=tag.name %}">
32 {{ tag.name }}</a>
40 33 {% endfor %}
41 34 <a class="tag" href="{% url 'tags' %}">[...]</a>
42 35 <a class="link" href="{% url 'settings' %}">{% trans 'Settings' %}</a>
43 36 </div>
44 37
45 38 {% block content %}{% endblock %}
46 39
47 40 <div class="navigation_panel">
48 41 {% block metapanel %}{% endblock %}
49 [<a href="rss/">RSS</a>]
42 [<a href="{% url "login" %}">{% trans 'Login' %}</a>]
50 43 <a class="link" href="#top">{% trans 'Up' %}</a>
51 44 </div>
52 45
53 46 </body>
54 47 </html>
@@ -1,22 +1,29 b''
1 <!DOCTYPE html>
2 <html>
3 <head>
4 <link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}css/md/login.css" media="all"/>
5 <title>Login page</title>
6 </head>
1 {% extends "boards/base.html" %}
2
3 {% load i18n %}
4
5 {% block head %}
6 <title>{% trans 'Login' %}</title>
7 {% endblock %}
8
9 {% block content %}
7 10
8 <body>
9 {% if error != none%}
10 <span id="error_message">
11 {{ error }}
12 </span>
13 {% endif %}
11 <form enctype="multipart/form-data" method="post">
12 <div class="post-form-w">
13 <div class="post-form">
14 <div class="form-row">
15 <div class="form-label">{% trans 'User ID' %}</div>
16 <div class="form-input">{{ form.user_id }}</div>
17 <div class="form-errors">{{ form.user_id.errors }}</div>
18 </div>
19 </div>
20 <div class="form-submit">
21 <input type="submit" value="{% trans "Login" %}"/>
22 </div>
23 <div>
24 {% trans 'Insert your user id above' %}
25 </div>
26 </div>
27 </form>
14 28
15 <form action="login" method="POST">{% csrf_token %}
16
17 Login: <input type="text" name="name"><br />
18 Password: <input type="password" name="password"><br />
19 <input type="submit">
20 </form>
21 </body>
22 </html> No newline at end of file
29 {% endblock %} No newline at end of file
@@ -1,168 +1,177 b''
1 1 {% extends "boards/base.html" %}
2 2
3 3 {% load i18n %}
4 4 {% load markup %}
5 5
6 6 {% block head %}
7 7 {% if tag %}
8 8 <title>Neboard - {{ tag }}</title>
9 9 {% else %}
10 10 <title>Neboard</title>
11 11 {% endif %}
12 12 {% endblock %}
13 13
14 14 {% block content %}
15 15
16 16 {% if tag %}
17 17 <div class="tag_info">
18 18 <h2>{% trans 'Tag: ' %}{{ tag }}</h2>
19 19 </div>
20 20 {% endif %}
21 21
22 22 {% if threads %}
23 23 {% for thread in threads %}
24 24 <div class="thread">
25 25 {% if thread.can_bump %}
26 26 <div class="post" id="{{thread.id}}">
27 27 {% else %}
28 28 <div class="post dead_post" id="{{ thread.id }}">
29 29 {% endif %}
30 30 {% if thread.image %}
31 31 <div class="image">
32 32 <a class="fancy"
33 33 href="{{ thread.image.url }}"><img
34 34 src="{{ thread.image.url_200x150 }}"
35 35 alt="{% trans 'Post image' %}"
36 36 data-width="{{ thread.image_width }}"
37 37 data-height="{{ thread.image_height }}" />
38 38 </a>
39 39 </div>
40 40 {% endif %}
41 41 <div class="message">
42 42 <div class="post-info">
43 43 <span class="title">{{ thread.title }}</span>
44 44 <a class="post_id" href="{% url 'thread' thread.id %}"
45 45 >(#{{ thread.id }})</a>
46 46 [{{ thread.pub_time }}]
47 47 [<a class="link" href="{% url 'thread' thread.id %}#form"
48 48 >{% trans "Reply" %}</a>]
49
50 {% if user.is_moderator %}
51 <span class="moderator_info">
52 ({{ thread.poster_ip }})
53 [<a href="{% url 'delete' post_id=thread.id %}"
54 >{% trans 'Delete' %}</a>]
55 </span>
56 {% endif %}
49 57 </div>
50 58 {% autoescape off %}
51 59 {{ thread.text.rendered|truncatewords_html:50 }}
52 60 {% endautoescape %}
53 61 </div>
54 62 <div class="metadata">
55 63 {{ thread.get_reply_count }} {% trans 'replies' %},
56 64 {{ thread.get_images_count }} {% trans 'images' %}.
57 65 {% if thread.tags.all %}
58 66 <span class="tags">{% trans 'Tags' %}:
59 67 {% for tag in thread.tags.all %}
60 68 <a class="tag" href="
61 69 {% url 'tag' tag_name=tag.name %}">
62 70 {{ tag.name }}</a>
63 71 {% endfor %}
64 72 </span>
65 73 {% endif %}
66 74 </div>
67 75 </div>
68 76 {% if thread.get_last_replies %}
69 77 <div class="last-replies">
70 78 {% for post in thread.get_last_replies %}
71 79 {% if thread.can_bump %}
72 80 <div class="post" id="{{ post.id }}">
73 81 {% else %}
74 82 <div class="post dead_post id="{{ post.id }}"">
75 83 {% endif %}
76 84 {% if post.image %}
77 85 <div class="image">
78 86 <a class="fancy"
79 87 href="{{ post.image.url }}"><img
80 88 src=" {{ post.image.url_200x150 }}"
81 89 alt="{% trans 'Post image' %}"
82 90 data-width="{{ post.image_width }}"
83 91 data-height="{{ post.image_height }}"/>
84 92 </a>
85 93 </div>
86 94 {% endif %}
87 95 <div class="message">
88 96 <div class="post-info">
89 97 <span class="title">{{ post.title }}</span>
90 98 <a class="post_id" href="
91 99 {% url 'thread' thread.id %}#{{ post.id }}">
92 100 (#{{ post.id }})</a>
93 101 [{{ post.pub_time }}]
94 102 </div>
95 103 {% autoescape off %}
96 104 {{ post.text.rendered|truncatewords_html:50 }}
97 105 {% endautoescape %}
98 106 </div>
99 107 </div>
100 108 {% endfor %}
101 109 </div>
102 110 {% endif %}
103 111 </div>
104 112 {% endfor %}
105 113 {% else %}
106 No threads found.
107 <hr />
114 <div class="post">
115 {% trans 'No threads exist. Create the first one!' %}</div>
108 116 {% endif %}
109 117
110 118 <form enctype="multipart/form-data" method="post">{% csrf_token %}
111 119 <div class="post-form-w">
112 120
113 121 <div class="form-title">{% trans "Create new thread" %}</div>
114 122 <div class="post-form">
115 123 <div class="form-row">
116 124 <div class="form-label">{% trans 'Title' %}</div>
117 125 <div class="form-input">{{ form.title }}</div>
118 126 <div class="form-errors">{{ form.title.errors }}</div>
119 127 </div>
120 128 <div class="form-row">
121 129 <div class="form-label">{% trans 'Text' %}</div>
122 130 <div class="form-input">{{ form.text }}</div>
123 131 <div class="form-errors">{{ form.text.errors }}</div>
124 132 </div>
125 133 <div class="form-row">
126 134 <div class="form-label">{% trans 'Image' %}</div>
127 135 <div class="form-input">{{ form.image }}</div>
128 136 <div class="form-errors">{{ form.image.errors }}</div>
129 137 </div>
130 138 <div class="form-row">
131 139 <div class="form-label">{% trans 'Tags' %}</div>
132 140 <div class="form-input">{{ form.tags }}</div>
133 141 <div class="form-errors">{{ form.tags.errors }}</div>
134 142 </div>
135 143 <div class="form-row">
136 144 {{ form.captcha }}
137 145 <div class="form-errors">{{ form.captcha.errors }}</div>
138 146 </div>
139 147 </div>
140 148 <div class="form-submit">
141 149 <input type="submit" value="{% trans "Post" %}"/></div>
142 150 <div>
143 151 {% trans 'Tags must be delimited by spaces. Text or image is required.' %}
144 152 </div>
145 153 <div><a href="http://daringfireball.net/projects/markdown/basics">
146 154 {% trans 'Basic markdown syntax.' %}</a></div>
147 155 </div>
148 156 </form>
149 157
150 158 {% endblock %}
151 159
152 160 {% block metapanel %}
153 161
154 162 <span class="metapanel">
155 163 <b><a href="{% url "authors" %}">Neboard</a> pre1.0</b>
156 164 {% trans "Pages:" %}
157 165 {% for page in pages %}
158 166 [<a href="
159 167 {% if tag %}
160 168 {% url "tag" tag_name=tag page=page %}
161 169 {% else %}
162 170 {% url "index" page=page %}
163 171 {% endif %}
164 172 ">{{ page }}</a>]
165 173 {% endfor %}
174 [<a href="rss/">RSS</a>]
166 175 </span>
167 176
168 177 {% endblock %}
@@ -1,34 +1,41 b''
1 1 {% extends "boards/base.html" %}
2 2
3 3 {% load i18n %}
4 4
5 5 {% block head %}
6 6 <title>Neboard settings</title>
7 7 {% endblock %}
8 8
9 9 {% block content %}
10 10
11 <div class="post">
12 {% trans 'User:' %} <b>{{ user.user_id }}</b>.
13 {% if user.is_moderator %}
14 {% trans 'You are moderator.' %}
15 {% endif %}
16 </div>
17
11 18 <div class="post-form-w">
12 19 <div class="post-form">
13 20 <span class="form-title">{% trans "Theme" %}</span>
14 21 <form method="post">{% csrf_token %}
15 22 {% for choice in form.fields.theme.choices %}
16 23 <div class="settings_item">
17 24 <label for="{{ choice.0 }}">
18 25 <input type="radio" name="theme"
19 26 id="{{ choice.0 }}"
20 27 value="{{ choice.0 }}"
21 28 {% ifequal form.initial.theme choice.0 %}
22 29 checked
23 30 {% endifequal %}
24 31 />
25 32 {{ choice.1 }}
26 33 </label>
27 34 </div>
28 35 {% endfor %}
29 36 <input type="submit" value="{% trans "Save" %}" />
30 37 </form>
31 38 </div>
32 39 </div>
33 40
34 41 {% endblock %} No newline at end of file
@@ -1,24 +1,32 b''
1 1 {% extends "boards/base.html" %}
2 2
3 3 {% load i18n %}
4 4 {% load markup %}
5 5
6 6 {% block head %}
7 <title>Neboard - {% trans "tags" %}</title>
7 <title>Neboard - {% trans "Tags" %}</title>
8 8 {% endblock %}
9 9
10 10 {% block content %}
11 11
12 12 <div class="post">
13 {% if tags %}
13 {% if all_tags %}
14 14 {% for tag in all_tags %}
15 15 <a class="tag" href="{% url 'tag' tag.name %}">
16 {{ tag.name }}</a><br />
16 {{ tag.name }}</a>
17 ({{ tag.get_post_count }} {% trans 'threads' %})
18 {% if tag in user.fav_tags.all %}
19 [<a href="{% url 'tag_unsubscribe' tag.name %}"
20 >{% trans 'Remove' %}</a>]
21 {% else %}
22 [<a href="{% url 'tag_subscribe' tag.name %}"
23 >{% trans 'Add' %}</a>]
24 {% endif %}
25 <br />
17 26 {% endfor %}
18 27 {% else %}
19 No tags found.
20 <hr />
28 {% trans 'No tags found.' %}
21 29 {% endif %}
22 30 </div>
23 31
24 32 {% endblock %} No newline at end of file
@@ -1,113 +1,119 b''
1 1 {% extends "boards/base.html" %}
2 2
3 3 {% load i18n %}
4 4 {% load markup %}
5 5
6 6 {% block head %}
7 7 <title>Neboard - {{ posts.0.title }}</title>
8 8 {% endblock %}
9 9
10 10 {% block content %}
11 11 <script src="{{ STATIC_URL }}js/thread.js"></script>
12 12
13 13 {% if posts %}
14 14 <div id="posts">
15 15 {% for post in posts %}
16 16 {% if posts.0.can_bump %}
17 17 <div class="post" id="{{ post.id }}">
18 18 {% else %}
19 19 <div class="post dead_post" id="{{ post.id }}">
20 20 {% endif %}
21 21 {% if post.image %}
22 22 <div class="image">
23 23 <a
24 24 class="fancy"
25 25 href="{{ post.image.url }}"><img
26 26 src="{{ post.image.url_200x150 }}"
27 27 alt="{% trans 'Post image' %}"
28 28 data-width="{{ post.image_width }}"
29 29 data-height="{{ post.image_height }}"/>
30 30 </a>
31 31 </div>
32 32 {% endif %}
33 33 <div class="message">
34 34 <div class="post-info">
35 35 <span class="title">{{ post.title }}</span>
36 36 <a class="post_id" href="#{{ post.id }}">
37 37 (#{{ post.id }})</a>
38 38 [{{ post.pub_time }}]
39 39 [<a href="#" onclick="javascript:addQuickReply('{{ post.id }}')
40 40 ; return false;">&gt;&gt;</a>]
41
42 {% if user.is_moderator %}
43 <span class="moderator_info">
44 ({{ post.poster_ip }})
45 [<a href="{% url 'delete' post_id=post.id %}"
46 >{% trans 'Delete' %}</a>]
47 </span>
48 {% endif %}
41 49 </div>
42 50 {% autoescape off %}
43 51 {{ post.text.rendered }}
44 52 {% endautoescape %}
45 53 </div>
46 54 {% if post.tags.all %}
47 55 <div class="metadata">
48 56 <span class="tags">{% trans 'Tags' %}:
49 57 {% for tag in post.tags.all %}
50 58 <a class="tag" href="{% url 'tag' tag.name %}">
51 59 {{ tag.name }}</a>
52 60 {% endfor %}
53 61 </span>
54 62 </div>
55 63 {% endif %}
56 64 </div>
57 65 {% endfor %}
58 66 </div>
59 {% else %}
60 No thread found.
61 <hr />
62 67 {% endif %}
63 68
64 69 <form id="form" enctype="multipart/form-data" method="post"
65 70 >{% csrf_token %}
66 71 <div class="post-form-w">
67 72 <div class="form-title">{% trans "Reply to thread" %} #{{ posts.0.id }}</div>
68 73 <div class="post-form">
69 74 <div class="form-row">
70 75 <div class="form-label">{% trans 'Title' %}</div>
71 76 <div class="form-input">{{ form.title }}</div>
72 77 <div class="form-errors">{{ form.title.errors }}</div>
73 78 </div>
74 79 <div class="form-row">
75 80 <div class="form-label">{% trans 'Text' %}</div>
76 81 <div class="form-input">{{ form.text }}</div>
77 82 <div class="form-errors">{{ form.text.errors }}</div>
78 83 </div>
79 84 <div class="form-row">
80 85 <div class="form-label">{% trans 'Image' %}</div>
81 86 <div class="form-input">{{ form.image }}</div>
82 87 <div class="form-errors">{{ form.image.errors }}</div>
83 88 </div>
84 89 <div class="form-row">
85 90 {{ form.captcha }}
86 91 <div class="form-errors">{{ form.captcha.errors }}</div>
87 92 </div>
88 93 </div>
89 94
90 95 <div class="form-submit"><input type="submit"
91 96 value="{% trans "Post" %}"/></div>
92 97 <div><a href="http://daringfireball.net/projects/markdown/basics">
93 98 {% trans 'Basic markdown syntax.' %}</a></div>
94 99 <div>{% trans 'Example: ' %}*<i>{% trans 'italic' %}</i>*,
95 100 **<b>{% trans 'bold' %}</b>**</div>
96 101 <div>{% trans 'Quotes can be inserted with' %} "&gt;"</div>
97 102 <div>{% trans 'Links to answers can be inserted with' %}
98 103 "&gt;&gt;123"
99 104 </div>
100 105 </div>
101 106 </form>
102 107
103 108 {% endblock %}
104 109
105 110 {% block metapanel %}
106 111
107 112 <span class="metapanel">
108 113 {{ posts.0.get_reply_count }} {% trans 'replies' %},
109 114 {{ posts.0.get_images_count }} {% trans 'images' %}.
110 115 {% trans 'Last update: ' %}{{ posts.0.last_edit_time }}
116 [<a href="rss/">RSS</a>]
111 117 </span>
112 118
113 119 {% endblock %}
@@ -1,225 +1,177 b''
1 1 # coding=utf-8
2 2 from django.utils.unittest import TestCase
3 3 from django.test.client import Client
4 4
5 5 import boards
6 6
7 from boards.models import Post, Admin, Tag
7 from boards.models import Post, Tag
8 8 from neboard import settings
9 9
10 10 TEST_TEXT = 'test text'
11 11
12 12 NEW_THREAD_PAGE = '/'
13 13 THREAD_PAGE_ONE = '/thread/1/'
14 14 THREAD_PAGE = '/thread/'
15 15 TAG_PAGE = '/tag/'
16 16 HTTP_CODE_REDIRECT = 302
17 17 HTTP_CODE_OK = 200
18 18 HTTP_CODE_NOT_FOUND = 404
19 19
20 20
21 21 class BoardTests(TestCase):
22 22 def _create_post(self):
23 23 return Post.objects.create_post(title='title',
24 24 text='text')
25 25
26 26 def test_post_add(self):
27 27 post = self._create_post()
28 28
29 29 self.assertIsNotNone(post)
30 30 self.assertEqual(boards.models.NO_PARENT, post.parent)
31 31
32 32 def test_delete_post(self):
33 33 post = self._create_post()
34 34 post_id = post.id
35 35
36 36 Post.objects.delete_post(post)
37 37
38 38 self.assertFalse(Post.objects.exists(post_id))
39 39
40 40 def test_delete_posts_by_ip(self):
41 41 post = self._create_post()
42 42 post_id = post.id
43 43
44 44 Post.objects.delete_posts_by_ip('0.0.0.0')
45 45
46 46 self.assertFalse(Post.objects.exists(post_id))
47 47
48 48 # Authentication tests
49 49
50 50 def _create_test_user(self):
51 51 admin = Admin(name='test_username12313584353165',
52 52 password='test_userpassword135135512')
53 53
54 54 admin.save()
55 55 return admin
56 56
57 def test_admin_login(self):
58 client = Client()
59
60 self.assertFalse('admin' in client.session)
61
62 admin = self._create_test_user()
63
64 response = client.post('/login',
65 {'name': admin.name, 'password': admin.password})
66
67 # it means that login passed and user are redirected to another page
68 self.assertEqual(302, response.status_code)
69
70 self.assertTrue('admin' in client.session)
71 self.assertTrue(client.session['admin'])
72
73 admin.delete()
74
75 wrong_name = 'sd2f1s3d21fs3d21f'
76 wrong_password = 'sd2f1s3d21fs3d21fsdfsd'
77
78 client.post('/login', {'name': wrong_name, 'password': wrong_password})
79 self.assertFalse(client.session['admin'])
80
81 def test_admin_logout(self):
82 client = Client()
83
84 self.assertFalse('admin' in client.session)
85
86 admin = self._create_test_user()
87
88 client.post('/login',
89 {'name': admin.name, 'password': admin.password})
90
91 self.assertTrue(client.session['admin'])
92
93 client.get('/logout')
94
95 self.assertFalse(client.session['admin'])
96
97 admin.delete()
98
99 57 def test_get_thread(self):
100 58 opening_post = self._create_post()
101 59 op_id = opening_post.id
102 60
103 61 for i in range(0, 2):
104 62 Post.objects.create_post('title', 'text',
105 63 parent_id=op_id)
106 64
107 65 thread = Post.objects.get_thread(op_id)
108 66
109 67 self.assertEqual(3, len(thread))
110 68
111 69 def test_create_post_with_tag(self):
112 70 tag = Tag.objects.create(name='test_tag')
113 71 post = Post.objects.create_post(title='title', text='text', tags=[tag])
114 72 self.assertIsNotNone(post)
115 73
116 74 def test_thread_max_count(self):
117 75 for i in range(settings.MAX_THREAD_COUNT + 1):
118 76 self._create_post()
119 77
120 78 self.assertEqual(settings.MAX_THREAD_COUNT,
121 79 len(Post.objects.get_threads()))
122 80
123 def test_get(self):
124 """Test if the get computes properly"""
125
126 post = self._create_post()
127
128 self.assertTrue(post.is_get())
129
130 81 def test_pages(self):
131 82 """Test that the thread list is properly split into pages"""
132 83
133 84 for i in range(settings.MAX_THREAD_COUNT):
134 85 self._create_post()
135 86
136 87 all_threads = Post.objects.get_threads()
137 88
138 89 posts_in_second_page = Post.objects.get_threads(page=1)
139 90 first_post = posts_in_second_page[0]
140 91
141 92 self.assertEqual(all_threads[settings.THREADS_PER_PAGE].id,
142 93 first_post.id)
143 94
144 95 def test_post_validation(self):
145 96 """Test the validation of the post form"""
146 97
147 98 # Disable captcha for the test
148 99 captcha_enabled = settings.ENABLE_CAPTCHA
149 100 settings.ENABLE_CAPTCHA = False
150 101
151 102 Post.objects.all().delete()
152 103
153 104 client = Client()
154 105
155 106 valid_tags = u'tag1 tag_2 Ρ‚Π΅Π³_3'
156 107 invalid_tags = u'$%_356 ---'
157 108
158 109 response = client.post(NEW_THREAD_PAGE, {'title': 'test title',
159 110 'text': TEST_TEXT,
160 111 'tags': valid_tags})
161 112 self.assertEqual(response.status_code, HTTP_CODE_REDIRECT,
162 113 msg='Posting new message failed: got code ' +
163 114 str(response.status_code))
164 115
165 116 self.assertEqual(1, Post.objects.count(),
166 117 msg='No posts were created')
167 118
168 119 client.post(NEW_THREAD_PAGE, {'text': TEST_TEXT,
169 120 'tags': invalid_tags})
170 121 self.assertEqual(1, Post.objects.count(), msg='The validation passed '
171 122 'where it should fail')
172 123
173 124 response = client.post(THREAD_PAGE_ONE, {'text': TEST_TEXT,
174 125 'tags': valid_tags})
175 126 self.assertEqual(HTTP_CODE_REDIRECT, response.status_code,
176 127 msg=u'Posting new message failed: got code ' +
177 128 str(response.status_code))
178 129
179 130 self.assertEqual(2, Post.objects.count(),
180 131 msg=u'No posts were created')
181 132
182 133 # Restore captcha setting
183 134 settings.ENABLE_CAPTCHA = captcha_enabled
184 135
185 136 def test_404(self):
186 137 """Test receiving error 404 when opening a non-existent page"""
187 138
188 139 Post.objects.all().delete()
189 140 Tag.objects.all().delete()
190 141
191 142 tag_name = u'test_tag'
192 143 tags, = [Tag.objects.get_or_create(name=tag_name)]
193 144 client = Client()
194 145
195 146 Post.objects.create_post('title', TEST_TEXT, tags=tags)
196 147
197 148 existing_post_id = Post.objects.all()[0].id
198 149 response_existing = client.get(THREAD_PAGE + str(existing_post_id) +
199 150 '/')
200 151 self.assertEqual(HTTP_CODE_OK, response_existing.status_code,
201 152 u'Cannot open existing thread')
202 153
203 154 response_not_existing = client.get(THREAD_PAGE + str(
204 155 existing_post_id + 1) + '/')
156 response_not_existing.get_full_path()
205 157 self.assertEqual(HTTP_CODE_NOT_FOUND,
206 158 response_not_existing.status_code,
207 159 u'Not existing thread is opened')
208 160
209 161 response_existing = client.get(TAG_PAGE + tag_name + '/')
210 162 self.assertEqual(HTTP_CODE_OK,
211 163 response_existing.status_code,
212 164 u'Cannot open existing tag')
213 165
214 166 response_not_existing = client.get(TAG_PAGE + u'not_tag' + '/')
215 167 self.assertEqual(HTTP_CODE_NOT_FOUND,
216 168 response_not_existing.status_code,
217 169 u'Not existing tag is opened')
218 170
219 171 reply_id = Post.objects.create_post('', TEST_TEXT,
220 172 parent_id=existing_post_id)
221 173 response_not_existing = client.get(THREAD_PAGE + str(
222 174 reply_id) + '/')
223 175 self.assertEqual(HTTP_CODE_NOT_FOUND,
224 176 response_not_existing.status_code,
225 177 u'Not existing thread is opened')
@@ -1,165 +1,173 b''
1 1 # -*- encoding: utf-8 -*-
2 2 """
3 3 django-thumbs by Antonio MelΓ©
4 4 http://django.es
5 5 """
6 6 from django.db.models import ImageField
7 7 from django.db.models.fields.files import ImageFieldFile
8 8 from PIL import Image
9 9 from django.core.files.base import ContentFile
10 10 import cStringIO
11 11
12
12 13 def generate_thumb(img, thumb_size, format):
13 14 """
14 15 Generates a thumbnail image and returns a ContentFile object with the thumbnail
15 16
16 17 Parameters:
17 18 ===========
18 19 img File object
19 20
20 21 thumb_size desired thumbnail size, ie: (200,120)
21 22
22 23 format format of the original image ('jpeg','gif','png',...)
23 24 (this format will be used for the generated thumbnail, too)
24 25 """
25
26
26 27 img.seek(0) # see http://code.djangoproject.com/ticket/8222 for details
27 28 image = Image.open(img)
28
29
29 30 # get size
30 31 thumb_w, thumb_h = thumb_size
31 32 # If you want to generate a square thumbnail
32 33 if thumb_w == thumb_h:
33 34 # quad
34 35 xsize, ysize = image.size
35 36 # get minimum size
36 minsize = min(xsize,ysize)
37 minsize = min(xsize, ysize)
37 38 # largest square possible in the image
38 xnewsize = (xsize-minsize)/2
39 ynewsize = (ysize-minsize)/2
39 xnewsize = (xsize - minsize) / 2
40 ynewsize = (ysize - minsize) / 2
40 41 # crop it
41 image2 = image.crop((xnewsize, ynewsize, xsize-xnewsize, ysize-ynewsize))
42 image2 = image.crop(
43 (xnewsize, ynewsize, xsize - xnewsize, ysize - ynewsize))
42 44 # load is necessary after crop
43 45 image2.load()
44 46 # thumbnail of the cropped image (with ANTIALIAS to make it look better)
45 47 image2.thumbnail(thumb_size, Image.ANTIALIAS)
46 48 else:
47 49 # not quad
48 50 image2 = image
49 51 image2.thumbnail(thumb_size, Image.ANTIALIAS)
50
52
51 53 io = cStringIO.StringIO()
52 54 # PNG and GIF are the same, JPG is JPEG
53 if format.upper()=='JPG':
55 if format.upper() == 'JPG':
54 56 format = 'JPEG'
55
57
56 58 image2.save(io, format)
57 return ContentFile(io.getvalue())
59 return ContentFile(io.getvalue())
60
58 61
59 62 class ImageWithThumbsFieldFile(ImageFieldFile):
60 63 """
61 64 See ImageWithThumbsField for usage example
62 65 """
66
63 67 def __init__(self, *args, **kwargs):
64 68 super(ImageWithThumbsFieldFile, self).__init__(*args, **kwargs)
65 69 self.sizes = self.field.sizes
66
70
67 71 if self.sizes:
68 72 def get_size(self, size):
69 73 if not self:
70 74 return ''
71 75 else:
72 split = self.url.rsplit('.',1)
73 thumb_url = '%s.%sx%s.%s' % (split[0],w,h,split[1])
76 split = self.url.rsplit('.', 1)
77 thumb_url = '%s.%sx%s.%s' % (split[0], w, h, split[1])
74 78 return thumb_url
75
79
76 80 for size in self.sizes:
77 (w,h) = size
78 setattr(self, 'url_%sx%s' % (w,h), get_size(self, size))
79
81 (w, h) = size
82 setattr(self, 'url_%sx%s' % (w, h), get_size(self, size))
83
80 84 def save(self, name, content, save=True):
81 85 super(ImageWithThumbsFieldFile, self).save(name, content, save)
82
86
83 87 if self.sizes:
84 88 for size in self.sizes:
85 (w,h) = size
86 split = self.name.rsplit('.',1)
87 thumb_name = '%s.%sx%s.%s' % (split[0],w,h,split[1])
88
89 (w, h) = size
90 split = self.name.rsplit('.', 1)
91 thumb_name = '%s.%sx%s.%s' % (split[0], w, h, split[1])
92
89 93 # you can use another thumbnailing function if you like
90 94 thumb_content = generate_thumb(content, size, split[1])
91
92 thumb_name_ = self.storage.save(thumb_name, thumb_content)
93
95
96 thumb_name_ = self.storage.save(thumb_name, thumb_content)
97
94 98 if not thumb_name == thumb_name_:
95 raise ValueError('There is already a file named %s' % thumb_name)
96
99 raise ValueError(
100 'There is already a file named %s' % thumb_name)
101
97 102 def delete(self, save=True):
98 name=self.name
103 name = self.name
99 104 super(ImageWithThumbsFieldFile, self).delete(save)
100 105 if self.sizes:
101 106 for size in self.sizes:
102 (w,h) = size
103 split = name.rsplit('.',1)
104 thumb_name = '%s.%sx%s.%s' % (split[0],w,h,split[1])
107 (w, h) = size
108 split = name.rsplit('.', 1)
109 thumb_name = '%s.%sx%s.%s' % (split[0], w, h, split[1])
105 110 try:
106 111 self.storage.delete(thumb_name)
107 112 except:
108 113 pass
109
114
115
110 116 class ImageWithThumbsField(ImageField):
111 117 attr_class = ImageWithThumbsFieldFile
112 118 """
113 119 Usage example:
114 120 ==============
115 121 photo = ImageWithThumbsField(upload_to='images', sizes=((125,125),(300,200),)
116 122
117 123 To retrieve image URL, exactly the same way as with ImageField:
118 124 my_object.photo.url
119 125 To retrieve thumbnails URL's just add the size to it:
120 126 my_object.photo.url_125x125
121 127 my_object.photo.url_300x200
122 128
123 129 Note: The 'sizes' attribute is not required. If you don't provide it,
124 130 ImageWithThumbsField will act as a normal ImageField
125 131
126 132 How it works:
127 133 =============
128 134 For each size in the 'sizes' atribute of the field it generates a
129 135 thumbnail with that size and stores it following this format:
130 136
131 137 available_filename.[width]x[height].extension
132 138
133 139 Where 'available_filename' is the available filename returned by the storage
134 140 backend for saving the original file.
135 141
136 142 Following the usage example above: For storing a file called "photo.jpg" it saves:
137 143 photo.jpg (original file)
138 144 photo.125x125.jpg (first thumbnail)
139 145 photo.300x200.jpg (second thumbnail)
140 146
141 147 With the default storage backend if photo.jpg already exists it will use these filenames:
142 148 photo_.jpg
143 149 photo_.125x125.jpg
144 150 photo_.300x200.jpg
145 151
146 152 Note: django-thumbs assumes that if filename "any_filename.jpg" is available
147 153 filenames with this format "any_filename.[widht]x[height].jpg" will be available, too.
148 154
149 155 To do:
150 156 ======
151 157 Add method to regenerate thubmnails
152 158
153 159
154 160 """
155 def __init__(self, verbose_name=None, name=None, width_field=None, height_field=None, sizes=None, **kwargs):
156 self.verbose_name=verbose_name
157 self.name=name
158 self.width_field=width_field
159 self.height_field=height_field
161
162 def __init__(self, verbose_name=None, name=None, width_field=None,
163 height_field=None, sizes=None, **kwargs):
164 self.verbose_name = verbose_name
165 self.name = name
166 self.width_field = width_field
167 self.height_field = height_field
160 168 self.sizes = sizes
161 169 super(ImageField, self).__init__(**kwargs)
162 170
163 171
164 172 from south.modelsinspector import add_introspection_rules
165 add_introspection_rules([], ["^boards\.thumbs\.ImageWithThumbsField"]) No newline at end of file
173 add_introspection_rules([], ["^boards\.thumbs\.ImageWithThumbsField"])
@@ -1,43 +1,50 b''
1 1 from django.conf.urls import patterns, url, include
2 2 from boards import views
3 3 from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed
4 4
5 5 js_info_dict = {
6 6 'packages': ('boards',),
7 7 }
8 8
9 9 urlpatterns = patterns('',
10 10
11 11 # /boards/
12 12 url(r'^$', views.index, name='index'),
13 13 # /boards/page/
14 14 url(r'^page/(?P<page>\w+)/$', views.index, name='index'),
15 15
16 16 # login page
17 url(r'^login$', views.login, name='login'),
18 # logout page
19 url(r'^logout$', views.logout, name='logout'),
17 url(r'^login/$', views.login, name='login'),
20 18
21 19 # /boards/tag/tag_name/
22 20 url(r'^tag/(?P<tag_name>\w+)/$', views.tag, name='tag'),
23 21 # /boards/tag/tag_id/page/
24 22 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/$', views.tag, name='tag'),
23
24 # /boards/tag/tag_name/unsubscribe/
25 url(r'^tag/(?P<tag_name>\w+)/subscribe/$', views.tag_subscribe,
26 name='tag_subscribe'),
27 # /boards/tag/tag_name/unsubscribe/
28 url(r'^tag/(?P<tag_name>\w+)/unsubscribe/$', views.tag_unsubscribe,
29 name='tag_unsubscribe'),
30
25 31 # /boards/thread/
26 32 url(r'^thread/(?P<post_id>\w+)/$', views.thread, name='thread'),
27 33 # /boards/theme/theme_name/
28 url(r'^settings$', views.settings, name='settings'),
29 url(r'^tags$', views.all_tags, name='tags'),
34 url(r'^settings/$', views.settings, name='settings'),
35 url(r'^tags/$', views.all_tags, name='tags'),
30 36 url(r'^captcha/', include('captcha.urls')),
31 37 url(r'^jump/(?P<post_id>\w+)/$', views.jump_to_post, name='jumper'),
32 38 url(r'^authors/$', views.authors, name='authors'),
39 url(r'^delete/(?P<post_id>\w+)/$', views.delete, name='delete'),
33 40 url(r'^banned/$', views.you_are_banned, name='banned'),
34 41
35 42 # RSS feeds
36 43 url(r'^rss/$', AllThreadsFeed()),
37 44 url(r'^page/(?P<page>\w+)/rss/$', AllThreadsFeed()),
38 45 url(r'^tag/(?P<tag_name>\w+)/rss/$', TagThreadsFeed()),
39 46 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/rss/$', TagThreadsFeed()),
40 47 url(r'^thread/(?P<post_id>\w+)/rss/$', ThreadPostsFeed()),
41 48
42 49 url(r'^jsi18n/$', 'django.views.i18n.javascript_catalog', js_info_dict),
43 ) No newline at end of file
50 )
@@ -1,261 +1,320 b''
1 import hashlib
1 2 from django.core.urlresolvers import reverse
2 3 from django.template import RequestContext
3 4 from django.shortcuts import render, redirect, get_object_or_404
4 5 from django.http import HttpResponseRedirect
6 from django.utils import timezone
5 7
6 8 from boards import forms
7 9 import boards
8 10 from boards import utils
9 11 from boards.forms import ThreadForm, PostForm, SettingsForm, PlainErrorList, \
10 ThreadCaptchaForm, PostCaptchaForm
12 ThreadCaptchaForm, PostCaptchaForm, LoginForm
11 13
12 from boards.models import Post, Admin, Tag, Ban
14 from boards.models import Post, Tag, Ban, User, RANK_USER, RANK_MODERATOR, NO_PARENT
13 15 from boards import authors
14 16 import neboard
15 17
16 18
17 19 def index(request, page=0):
18 20 context = _init_default_context(request)
19 21
20 22 if utils.need_include_captcha(request):
21 23 threadFormClass = ThreadCaptchaForm
22 24 kwargs = {'request': request}
23 25 else:
24 26 threadFormClass = ThreadForm
25 27 kwargs = {}
26 28
27 29 if request.method == 'POST':
28 30 form = threadFormClass(request.POST, request.FILES,
29 31 error_class=PlainErrorList, **kwargs)
30 32
31 33 if form.is_valid():
32 34 return _new_post(request, form)
33 35 else:
34 36 form = threadFormClass(error_class=PlainErrorList, **kwargs)
35 37
36 38 threads = Post.objects.get_threads(page=int(page))
37 39
38 40 context['threads'] = None if len(threads) == 0 else threads
39 41 context['form'] = form
40 42 context['pages'] = range(Post.objects.get_thread_page_count())
41 43
42 44 return render(request, 'boards/posting_general.html',
43 45 context)
44 46
45 47
46 48 def _new_post(request, form, thread_id=boards.models.NO_PARENT):
47 49 """Add a new post (in thread or as a reply)."""
48 50
49 51 ip = _get_client_ip(request)
50 52 is_banned = Ban.objects.filter(ip=ip).count() > 0
51 53
52 54 if is_banned:
53 55 return redirect(you_are_banned)
54 56
55 57 data = form.cleaned_data
56 58
57 59 title = data['title']
58 60 text = data['text']
59 61
60 62 if 'image' in data.keys():
61 63 image = data['image']
62 64 else:
63 65 image = None
64 66
65 67 tags = []
66 68
67 69 new_thread = thread_id == boards.models.NO_PARENT
68 70 if new_thread:
69 71 tag_strings = data['tags']
70 72
71 73 if tag_strings:
72 74 tag_strings = tag_strings.split(' ')
73 75 for tag_name in tag_strings:
74 76 tag_name = tag_name.strip()
75 77 if len(tag_name) > 0:
76 78 tag, created = Tag.objects.get_or_create(name=tag_name)
77 79 tags.append(tag)
78 80
79 81 # TODO Add a possibility to define a link image instead of an image file.
80 82 # If a link is given, download the image automatically.
81 83
82 84 post = Post.objects.create_post(title=title, text=text, ip=ip,
83 85 parent_id=thread_id, image=image,
84 86 tags=tags)
85 87
86 88 thread_to_show = (post.id if new_thread else thread_id)
87 89
88 90 if new_thread:
89 91 return redirect(thread, post_id=thread_to_show)
90 92 else:
91 93 return redirect(reverse(thread,
92 94 kwargs={'post_id': thread_to_show}) + '#'
93 95 + str(post.id))
94 96
95 97
96 98 def tag(request, tag_name, page=0):
97 99 """Get all tag threads (posts without a parent)."""
98 100
99 101 tag = get_object_or_404(Tag, name=tag_name)
100 102 threads = Post.objects.get_threads(tag=tag, page=int(page))
101 103
102 104 if request.method == 'POST':
103 105 form = ThreadForm(request.POST, request.FILES,
104 106 error_class=PlainErrorList)
105 107 if form.is_valid():
106 108 return _new_post(request, form)
107 109 else:
108 110 form = forms.ThreadForm(initial={'tags': tag_name},
109 111 error_class=PlainErrorList)
110 112
111 113 context = _init_default_context(request)
112 114 context['threads'] = None if len(threads) == 0 else threads
113 115 context['tag'] = tag_name
114 116 context['pages'] = range(Post.objects.get_thread_page_count(tag=tag))
115 117
116 118 context['form'] = form
117 119
118 120 return render(request, 'boards/posting_general.html',
119 121 context)
120 122
121 123
122 124 def thread(request, post_id):
123 125 """Get all thread posts"""
124 126
125 127 if utils.need_include_captcha(request):
126 128 postFormClass = PostCaptchaForm
127 129 kwargs = {'request': request}
128 130 else:
129 131 postFormClass = PostForm
130 132 kwargs = {}
131 133
132 134 if request.method == 'POST':
133 135 form = postFormClass(request.POST, request.FILES,
134 136 error_class=PlainErrorList, **kwargs)
135 137 if form.is_valid():
136 138 return _new_post(request, form, post_id)
137 139 else:
138 140 form = postFormClass(error_class=PlainErrorList, **kwargs)
139 141
140 142 posts = Post.objects.get_thread(post_id)
141 143
142 144 context = _init_default_context(request)
143 145
144 146 context['posts'] = posts
145 147 context['form'] = form
146 148
147 149 return render(request, 'boards/thread.html', context)
148 150
149 151
150 152 def login(request):
151 """Log in as admin"""
152
153 if 'name' in request.POST and 'password' in request.POST:
154 request.session['admin'] = False
153 """Log in with user id"""
155 154
156 isAdmin = len(Admin.objects.filter(name=request.POST['name'],
157 password=request.POST[
158 'password'])) > 0
159
160 if isAdmin:
161 request.session['admin'] = True
155 context = _init_default_context(request)
162 156
163 response = HttpResponseRedirect('/')
164
165 else:
166 response = render(request, 'boards/login.html', {'error': 'Login error'})
167 else:
168 response = render(request, 'boards/login.html', {})
157 if request.method == 'POST':
158 form = LoginForm(request.POST, request.FILES, error_class=PlainErrorList)
159 if form.is_valid():
160 user = User.objects.get(user_id=form.cleaned_data['user_id'])
161 request.session['user_id'] = user.id
162 return redirect(index)
169 163
170 return response
171
164 else:
165 form = LoginForm()
172 166
173 def logout(request):
174 request.session['admin'] = False
175 return HttpResponseRedirect('/')
167 context['form'] = form
168
169 return render(request, 'boards/login.html', context)
176 170
177 171
178 172 def settings(request):
179 173 """User's settings"""
180 174
181 context = RequestContext(request)
175 context = _init_default_context(request)
182 176
183 177 if request.method == 'POST':
184 178 form = SettingsForm(request.POST)
185 179 if form.is_valid():
186 180 selected_theme = form.cleaned_data['theme']
187 request.session['theme'] = selected_theme
181
182 user = _get_user(request)
183 user.save_setting('theme', selected_theme)
188 184
189 185 return redirect(settings)
190 186 else:
191 187 selected_theme = _get_theme(request)
192 188 form = SettingsForm(initial={'theme': selected_theme})
193 189 context['form'] = form
194 context['tags'] = Tag.objects.get_popular_tags()
195 context['theme'] = _get_theme(request)
196 190
197 191 return render(request, 'boards/settings.html', context)
198 192
199 193
200 194 def all_tags(request):
201 195 """All tags list"""
202 196
203 197 context = _init_default_context(request)
204 198 context['all_tags'] = Tag.objects.get_not_empty_tags()
205 199
206 200 return render(request, 'boards/tags.html', context)
207 201
208 202
209 203 def jump_to_post(request, post_id):
210 204 """Determine thread in which the requested post is and open it's page"""
211 205
212 206 post = get_object_or_404(Post, id=post_id)
213 207
214 208 if boards.models.NO_PARENT == post.parent:
215 209 return redirect(thread, post_id=post.id)
216 210 else:
217 211 parent_thread = get_object_or_404(Post, id=post.parent)
218 212 return redirect(reverse(thread, kwargs={'post_id': parent_thread.id})
219 213 + '#' + str(post.id))
220 214
221 215
222 216 def authors(request):
223 217 context = _init_default_context(request)
224 218 context['authors'] = boards.authors.authors
225 219
226 220 return render(request, 'boards/authors.html', context)
227 221
228 222
223 def delete(request, post_id):
224 user = _get_user(request)
225 post = get_object_or_404(Post, id=post_id)
226
227 if user.is_moderator():
228 Post.objects.delete_post(post)
229
230 if NO_PARENT == post.parent:
231 return redirect(index)
232 else:
233 return redirect(thread, post_id=post.parent)
234
235
229 236 def you_are_banned(request):
230 237 context = _init_default_context(request)
231 238 return render(request, 'boards/banned.html', context)
232 239
233 240
234 241 def page_404(request):
235 242 context = _init_default_context(request)
236 243 return render(request, 'boards/404.html', context)
237 244
238 245
246 def tag_subscribe(request, tag_name):
247 user = _get_user(request)
248 tag = get_object_or_404(Tag, name=tag_name)
249
250 if not tag in user.fav_tags.all():
251 user.fav_tags.add(tag)
252
253 return redirect(all_tags)
254
255
256 def tag_unsubscribe(request, tag_name):
257 user = _get_user(request)
258 tag = get_object_or_404(Tag, name=tag_name)
259
260 if tag in user.fav_tags.all():
261 user.fav_tags.remove(tag)
262
263 return redirect(all_tags)
264
265
239 266 def _get_theme(request):
240 267 """Get user's CSS theme"""
241 268
242 return request.session.get('theme', neboard.settings.DEFAULT_THEME)
269 user = _get_user(request)
270 theme = user.get_setting('theme')
271 if not theme:
272 theme = neboard.settings.DEFAULT_THEME
273
274 return theme
243 275
244 276
245 277 def _get_client_ip(request):
246 278 x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
247 279 if x_forwarded_for:
248 280 ip = x_forwarded_for.split(',')[-1].strip()
249 281 else:
250 282 ip = request.META.get('REMOTE_ADDR')
251 283 return ip
252 284
253 285
254 286 def _init_default_context(request):
255 287 """Create context with default values that are used in most views"""
256 288
257 289 context = RequestContext(request)
258 context['tags'] = Tag.objects.get_popular_tags()
290 context['user'] = _get_user(request)
291 context['tags'] = _get_user(request).fav_tags.all()
259 292 context['theme'] = _get_theme(request)
260 293
261 294 return context
295
296
297 def _get_user(request):
298 """Get current user from the session"""
299
300 session = request.session
301 if not 'user_id' in session:
302 request.session.save()
303
304 md5 = hashlib.md5()
305 md5.update(session.session_key)
306 new_id = md5.hexdigest()
307
308 time_now = timezone.now()
309 user = User.objects.create(user_id=new_id, rank=RANK_USER,
310 registration_time=time_now,
311 last_access_time=time_now)
312
313 session['user_id'] = user.id
314 else:
315 user = User.objects.get(id=session['user_id'])
316 user.save()
317
318 user.last_access_time = timezone.now()
319
320 return user
@@ -1,200 +1,201 b''
1 1 # Django settings for neboard project.
2 2 import os
3 3 import markdown
4 4 from boards.mdx_neboard import markdown_extended
5 5
6 6 DEBUG = True
7 7 TEMPLATE_DEBUG = DEBUG
8 8
9 9 ADMINS = (
10 10 # ('Your Name', 'your_email@example.com'),
11 11 ('admin', 'admin@example.com')
12 12 )
13 13
14 14 MANAGERS = ADMINS
15 15
16 16 DATABASES = {
17 17 'default': {
18 18 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
19 19 'NAME': 'database.db', # Or path to database file if using sqlite3.
20 20 'USER': '', # Not used with sqlite3.
21 21 'PASSWORD': '', # Not used with sqlite3.
22 22 'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
23 23 'PORT': '', # Set to empty string for default. Not used with sqlite3.
24 24 }
25 25 }
26 26
27 27 # Local time zone for this installation. Choices can be found here:
28 28 # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
29 29 # although not all choices may be available on all operating systems.
30 30 # In a Windows environment this must be set to your system time zone.
31 31 TIME_ZONE = 'Europe/Kiev'
32 32
33 33 # Language code for this installation. All choices can be found here:
34 34 # http://www.i18nguy.com/unicode/language-identifiers.html
35 35 LANGUAGE_CODE = 'en'
36 36
37 37 SITE_ID = 1
38 38
39 39 # If you set this to False, Django will make some optimizations so as not
40 40 # to load the internationalization machinery.
41 41 USE_I18N = True
42 42
43 43 # If you set this to False, Django will not format dates, numbers and
44 44 # calendars according to the current locale.
45 45 USE_L10N = True
46 46
47 47 # If you set this to False, Django will not use timezone-aware datetimes.
48 48 USE_TZ = True
49 49
50 50 # Absolute filesystem path to the directory that will hold user-uploaded files.
51 51 # Example: "/home/media/media.lawrence.com/media/"
52 52 MEDIA_ROOT = './media/'
53 53
54 54 # URL that handles the media served from MEDIA_ROOT. Make sure to use a
55 55 # trailing slash.
56 56 # Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
57 57 MEDIA_URL = '/media/'
58 58
59 59 # Absolute path to the directory static files should be collected to.
60 60 # Don't put anything in this directory yourself; store your static files
61 61 # in apps' "static/" subdirectories and in STATICFILES_DIRS.
62 62 # Example: "/home/media/media.lawrence.com/static/"
63 63 STATIC_ROOT = ''
64 64
65 65 # URL prefix for static files.
66 66 # Example: "http://media.lawrence.com/static/"
67 67 STATIC_URL = '/static/'
68 68
69 69 # Additional locations of static files
70 70 # It is really a hack, put real paths, not related
71 71 STATICFILES_DIRS = (
72 72 os.path.dirname(__file__) + '/boards/static',
73 73
74 74 # '/d/work/python/django/neboard/neboard/boards/static',
75 75 # Put strings here, like "/home/html/static" or "C:/www/django/static".
76 76 # Always use forward slashes, even on Windows.
77 77 # Don't forget to use absolute paths, not relative paths.
78 78 )
79 79
80 80 # List of finder classes that know how to find static files in
81 81 # various locations.
82 82 STATICFILES_FINDERS = (
83 83 'django.contrib.staticfiles.finders.FileSystemFinder',
84 84 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
85 85 # 'django.contrib.staticfiles.finders.DefaultStorageFinder',
86 86 )
87 87
88 88 # Make this unique, and don't share it with anybody.
89 89 SECRET_KEY = '@1rc$o(7=tt#kd+4s$u6wchm**z^)4x90)7f6z(i&amp;55@o11*8o'
90 90
91 91 # List of callables that know how to import templates from various sources.
92 92 TEMPLATE_LOADERS = (
93 93 'django.template.loaders.filesystem.Loader',
94 94 'django.template.loaders.app_directories.Loader',
95 95 # 'django.template.loaders.eggs.Loader',
96 96 )
97 97
98 98 TEMPLATE_CONTEXT_PROCESSORS = (
99 99 'django.core.context_processors.media',
100 100 'django.core.context_processors.static',
101 101 'django.core.context_processors.request',
102 102 'django.contrib.auth.context_processors.auth',
103 103 )
104 104
105 105 MIDDLEWARE_CLASSES = (
106 106 'django.contrib.sessions.middleware.SessionMiddleware',
107 107 'django.middleware.locale.LocaleMiddleware',
108 108 'django.middleware.common.CommonMiddleware',
109 109 # 'django.middleware.csrf.CsrfViewMiddleware',
110 110 'django.contrib.auth.middleware.AuthenticationMiddleware',
111 111 'django.contrib.messages.middleware.MessageMiddleware',
112 112 # Uncomment the next line for simple clickjacking protection:
113 113 # 'django.middleware.clickjacking.XFrameOptionsMiddleware',
114 114 )
115 115
116 116 ROOT_URLCONF = 'neboard.urls'
117 117
118 118 # Python dotted path to the WSGI application used by Django's runserver.
119 119 WSGI_APPLICATION = 'neboard.wsgi.application'
120 120
121 121 TEMPLATE_DIRS = (
122 122 # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
123 123 # Always use forward slashes, even on Windows.
124 124 # Don't forget to use absolute paths, not relative paths.
125 125 'templates',
126 126 )
127 127
128 128 INSTALLED_APPS = (
129 129 'django.contrib.auth',
130 130 'django.contrib.contenttypes',
131 131 'django.contrib.sessions',
132 132 # 'django.contrib.sites',
133 133 'django.contrib.messages',
134 134 'django.contrib.staticfiles',
135 135 # Uncomment the next line to enable the admin:
136 136 'django.contrib.admin',
137 137 # Uncomment the next line to enable admin documentation:
138 138 # 'django.contrib.admindocs',
139 139 'django.contrib.markup',
140 140 'django_cleanup',
141 141 'boards',
142 142 'captcha',
143 143 'south',
144 144 )
145 145
146 146 # TODO: NEED DESIGN FIXES
147 147 CAPTCHA_OUTPUT_FORMAT = (u' %(hidden_field)s '
148 148 u'<div class="form-label">%(image)s</div>'
149 149 u'<div class="form-text">%(text_field)s</div>')
150 150
151 151 # A sample logging configuration. The only tangible logging
152 152 # performed by this configuration is to send an email to
153 153 # the site admins on every HTTP 500 error when DEBUG=False.
154 154 # See http://docs.djangoproject.com/en/dev/topics/logging for
155 155 # more details on how to customize your logging configuration.
156 156 LOGGING = {
157 157 'version': 1,
158 158 'disable_existing_loggers': False,
159 159 'filters': {
160 160 'require_debug_false': {
161 161 '()': 'django.utils.log.RequireDebugFalse'
162 162 }
163 163 },
164 164 'handlers': {
165 165 'mail_admins': {
166 166 'level': 'ERROR',
167 167 'filters': ['require_debug_false'],
168 168 'class': 'django.utils.log.AdminEmailHandler'
169 169 }
170 170 },
171 171 'loggers': {
172 172 'django.request': {
173 173 'handlers': ['mail_admins'],
174 174 'level': 'ERROR',
175 175 'propagate': True,
176 176 },
177 177 }
178 178 }
179 179
180 180 MARKUP_FIELD_TYPES = (
181 181 ('markdown', markdown_extended),
182 182 )
183 183 # Custom imageboard settings
184 184 MAX_POSTS_PER_THREAD = 10 # Thread bumplimit
185 185 MAX_THREAD_COUNT = 500 # Old threads will be deleted to preserve this count
186 186 THREADS_PER_PAGE = 10
187 187 SITE_NAME = 'Neboard'
188 188
189 189 THEMES = [
190 190 ('md', 'Mystic Dark'),
191 ('sw', 'Snow White') ]
191 ('sw', 'Snow White')
192 ]
192 193
193 194 DEFAULT_THEME = 'md'
194 195
195 196 POPULAR_TAGS = 10
196 197 LAST_REPLIES_COUNT = 3
197 198
198 ENABLE_CAPTCHA = True
199 ENABLE_CAPTCHA = False
199 200 # if user tries to post before CAPTCHA_DEFAULT_SAFE_TIME. Captcha will be shown
200 201 CAPTCHA_DEFAULT_SAFE_TIME = 30 # seconds
General Comments 0
You need to be logged in to leave comments. Login now