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