##// END OF EJS Templates
Merged in 2.0 branch
neko259 -
r747:668c0b11 merge default
parent child Browse files
Show More
@@ -0,0 +1,143 b''
1 from django.shortcuts import get_object_or_404
2 from boards.models import Tag
3
4 __author__ = 'neko259'
5
6 SESSION_SETTING = 'setting'
7
8 PERMISSION_MODERATE = 'moderator'
9
10 SETTING_THEME = 'theme'
11 SETTING_FAVORITE_TAGS = 'favorite_tags'
12 SETTING_HIDDEN_TAGS = 'hidden_tags'
13 SETTING_PERMISSIONS = 'permissions'
14
15 DEFAULT_THEME = 'md'
16
17
18 def get_settings_manager(request):
19 """
20 Get settings manager based on the request object. Currently only
21 session-based manager is supported. In the future, cookie-based or
22 database-based managers could be implemented.
23 """
24 return SessionSettingsManager(request.session)
25
26
27 class SettingsManager:
28 """
29 Base settings manager class. get_setting and set_setting methods should
30 be overriden.
31 """
32 def __init__(self):
33 pass
34
35 def get_theme(self):
36 theme = self.get_setting(SETTING_THEME)
37 if not theme:
38 theme = DEFAULT_THEME
39 self.set_setting(SETTING_THEME, theme)
40
41 return theme
42
43 def set_theme(self, theme):
44 self.set_setting(SETTING_THEME, theme)
45
46 def has_permission(self, permission):
47 permissions = self.get_setting(SETTING_PERMISSIONS)
48 if permissions:
49 return permission in permissions
50 else:
51 return False
52
53 def get_setting(self, setting):
54 pass
55
56 def set_setting(self, setting, value):
57 pass
58
59 def add_permission(self, permission):
60 permissions = self.get_setting(SETTING_PERMISSIONS)
61 if not permissions:
62 permissions = [permission]
63 else:
64 permissions.append(permission)
65 self.set_setting(SETTING_PERMISSIONS, permissions)
66
67 def del_permission(self, permission):
68 permissions = self.get_setting(SETTING_PERMISSIONS)
69 if not permissions:
70 permissions = []
71 else:
72 permissions.remove(permission)
73 self.set_setting(SETTING_PERMISSIONS, permissions)
74
75 def get_fav_tags(self):
76 tag_names = self.get_setting(SETTING_FAVORITE_TAGS)
77 tags = []
78 if tag_names:
79 for tag_name in tag_names:
80 tag = get_object_or_404(Tag, name=tag_name)
81 tags.append(tag)
82
83 return tags
84
85 def add_fav_tag(self, tag):
86 tags = self.get_setting(SETTING_FAVORITE_TAGS)
87 if not tags:
88 tags = [tag.name]
89 else:
90 if not tag.name in tags:
91 tags.append(tag.name)
92 self.set_setting(SETTING_FAVORITE_TAGS, tags)
93
94 def del_fav_tag(self, tag):
95 tags = self.get_setting(SETTING_FAVORITE_TAGS)
96 if tag.name in tags:
97 tags.remove(tag.name)
98 self.set_setting(SETTING_FAVORITE_TAGS, tags)
99
100 def get_hidden_tags(self):
101 tag_names = self.get_setting(SETTING_HIDDEN_TAGS)
102 tags = []
103 if tag_names:
104 for tag_name in tag_names:
105 tag = get_object_or_404(Tag, name=tag_name)
106 tags.append(tag)
107
108 return tags
109
110 def add_hidden_tag(self, tag):
111 tags = self.get_setting(SETTING_HIDDEN_TAGS)
112 if not tags:
113 tags = [tag.name]
114 else:
115 if not tag.name in tags:
116 tags.append(tag.name)
117 self.set_setting(SETTING_HIDDEN_TAGS, tags)
118
119 def del_hidden_tag(self, tag):
120 tags = self.get_setting(SETTING_HIDDEN_TAGS)
121 if tag.name in tags:
122 tags.remove(tag.name)
123 self.set_setting(SETTING_HIDDEN_TAGS, tags)
124
125
126 class SessionSettingsManager(SettingsManager):
127 """
128 Session-based settings manager. All settings are saved to the user's
129 session.
130 """
131 def __init__(self, session):
132 SettingsManager.__init__(self)
133 self.session = session
134
135 def get_setting(self, setting):
136 if setting in self.session:
137 return self.session[setting]
138 else:
139 return None
140
141 def set_setting(self, setting, value):
142 self.session[setting] = value
143
@@ -0,0 +1,146 b''
1 # -*- coding: utf-8 -*-
2 from south.utils import datetime_utils as datetime
3 from south.db import db
4 from south.v2 import SchemaMigration
5 from django.db import models
6
7
8 class Migration(SchemaMigration):
9
10 def forwards(self, orm):
11 # Deleting model 'Setting'
12 db.delete_table(u'boards_setting')
13
14 # Deleting model 'User'
15 db.delete_table(u'boards_user')
16
17 # Removing M2M table for field hidden_threads on 'User'
18 db.delete_table(db.shorten_name(u'boards_user_hidden_threads'))
19
20 # Removing M2M table for field fav_threads on 'User'
21 db.delete_table(db.shorten_name(u'boards_user_fav_threads'))
22
23 # Removing M2M table for field fav_tags on 'User'
24 db.delete_table(db.shorten_name(u'boards_user_fav_tags'))
25
26 # Removing M2M table for field hidden_tags on 'User'
27 db.delete_table(db.shorten_name(u'boards_user_hidden_tags'))
28
29 # Deleting field 'Post.user'
30 db.delete_column(u'boards_post', 'user_id')
31
32
33 def backwards(self, orm):
34 # Adding model 'Setting'
35 db.create_table(u'boards_setting', (
36 ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['boards.User'])),
37 (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
38 ('value', self.gf('django.db.models.fields.CharField')(max_length=50)),
39 ('name', self.gf('django.db.models.fields.CharField')(max_length=50)),
40 ))
41 db.send_create_signal('boards', ['Setting'])
42
43 # Adding model 'User'
44 db.create_table(u'boards_user', (
45 ('registration_time', self.gf('django.db.models.fields.DateTimeField')()),
46 (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
47 ('user_id', self.gf('django.db.models.fields.CharField')(max_length=50)),
48 ('rank', self.gf('django.db.models.fields.IntegerField')()),
49 ))
50 db.send_create_signal('boards', ['User'])
51
52 # Adding M2M table for field hidden_threads on 'User'
53 m2m_table_name = db.shorten_name(u'boards_user_hidden_threads')
54 db.create_table(m2m_table_name, (
55 ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
56 ('user', models.ForeignKey(orm['boards.user'], null=False)),
57 ('post', models.ForeignKey(orm['boards.post'], null=False))
58 ))
59 db.create_unique(m2m_table_name, ['user_id', 'post_id'])
60
61 # Adding M2M table for field fav_threads on 'User'
62 m2m_table_name = db.shorten_name(u'boards_user_fav_threads')
63 db.create_table(m2m_table_name, (
64 ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
65 ('user', models.ForeignKey(orm['boards.user'], null=False)),
66 ('post', models.ForeignKey(orm['boards.post'], null=False))
67 ))
68 db.create_unique(m2m_table_name, ['user_id', 'post_id'])
69
70 # Adding M2M table for field fav_tags on 'User'
71 m2m_table_name = db.shorten_name(u'boards_user_fav_tags')
72 db.create_table(m2m_table_name, (
73 ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
74 ('user', models.ForeignKey(orm['boards.user'], null=False)),
75 ('tag', models.ForeignKey(orm['boards.tag'], null=False))
76 ))
77 db.create_unique(m2m_table_name, ['user_id', 'tag_id'])
78
79 # Adding M2M table for field hidden_tags on 'User'
80 m2m_table_name = db.shorten_name(u'boards_user_hidden_tags')
81 db.create_table(m2m_table_name, (
82 ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
83 ('user', models.ForeignKey(orm['boards.user'], null=False)),
84 ('tag', models.ForeignKey(orm['boards.tag'], null=False))
85 ))
86 db.create_unique(m2m_table_name, ['user_id', 'tag_id'])
87
88 # Adding field 'Post.user'
89 db.add_column(u'boards_post', 'user',
90 self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['boards.User'], null=True),
91 keep_default=False)
92
93
94 models = {
95 'boards.ban': {
96 'Meta': {'object_name': 'Ban'},
97 'can_read': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
98 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
99 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
100 'reason': ('django.db.models.fields.CharField', [], {'default': "'Auto'", 'max_length': '200'})
101 },
102 'boards.post': {
103 'Meta': {'ordering': "('id',)", 'object_name': 'Post'},
104 '_text_rendered': ('django.db.models.fields.TextField', [], {}),
105 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
106 'images': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'ip+'", 'to': "orm['boards.PostImage']", 'blank': 'True', 'symmetrical': 'False', 'null': 'True', 'db_index': 'True'}),
107 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}),
108 'poster_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
109 'poster_user_agent': ('django.db.models.fields.TextField', [], {}),
110 'pub_time': ('django.db.models.fields.DateTimeField', [], {}),
111 'referenced_posts': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'rfp+'", 'to': "orm['boards.Post']", 'blank': 'True', 'symmetrical': 'False', 'null': 'True', 'db_index': 'True'}),
112 'refmap': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
113 'text': ('markupfield.fields.MarkupField', [], {'rendered_field': 'True'}),
114 'text_markup_type': ('django.db.models.fields.CharField', [], {'default': "'markdown'", 'max_length': '30'}),
115 'thread_new': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.Thread']", 'null': 'True'}),
116 'title': ('django.db.models.fields.CharField', [], {'max_length': '200'})
117 },
118 'boards.postimage': {
119 'Meta': {'ordering': "('id',)", 'object_name': 'PostImage'},
120 'hash': ('django.db.models.fields.CharField', [], {'max_length': '36'}),
121 'height': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
122 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
123 'image': ('boards.thumbs.ImageWithThumbsField', [], {'max_length': '100', 'blank': 'True'}),
124 'pre_height': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
125 'pre_width': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
126 'width': ('django.db.models.fields.IntegerField', [], {'default': '0'})
127 },
128 'boards.tag': {
129 'Meta': {'ordering': "('name',)", 'object_name': 'Tag'},
130 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
131 'linked': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['boards.Tag']", 'null': 'True', 'blank': 'True'}),
132 'name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}),
133 'threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'tag+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Thread']"})
134 },
135 'boards.thread': {
136 'Meta': {'object_name': 'Thread'},
137 'archived': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
138 'bump_time': ('django.db.models.fields.DateTimeField', [], {}),
139 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
140 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}),
141 'replies': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'tre+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
142 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['boards.Tag']", 'symmetrical': 'False'})
143 }
144 }
145
146 complete_apps = ['boards'] No newline at end of file
@@ -0,0 +1,73 b''
1 # -*- coding: utf-8 -*-
2 from south.utils import datetime_utils as datetime
3 from south.db import db
4 from south.v2 import SchemaMigration
5 from django.db import models
6
7
8 class Migration(SchemaMigration):
9
10 def forwards(self, orm):
11 # Deleting field 'Tag.linked'
12 db.delete_column(u'boards_tag', 'linked_id')
13
14
15 def backwards(self, orm):
16 # Adding field 'Tag.linked'
17 db.add_column(u'boards_tag', 'linked',
18 self.gf('django.db.models.fields.related.ForeignKey')(to=orm['boards.Tag'], null=True, blank=True),
19 keep_default=False)
20
21
22 models = {
23 'boards.ban': {
24 'Meta': {'object_name': 'Ban'},
25 'can_read': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
26 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
27 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
28 'reason': ('django.db.models.fields.CharField', [], {'default': "'Auto'", 'max_length': '200'})
29 },
30 'boards.post': {
31 'Meta': {'ordering': "('id',)", 'object_name': 'Post'},
32 '_text_rendered': ('django.db.models.fields.TextField', [], {}),
33 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
34 'images': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'ip+'", 'to': "orm['boards.PostImage']", 'blank': 'True', 'symmetrical': 'False', 'null': 'True', 'db_index': 'True'}),
35 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}),
36 'poster_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
37 'poster_user_agent': ('django.db.models.fields.TextField', [], {}),
38 'pub_time': ('django.db.models.fields.DateTimeField', [], {}),
39 'referenced_posts': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'rfp+'", 'to': "orm['boards.Post']", 'blank': 'True', 'symmetrical': 'False', 'null': 'True', 'db_index': 'True'}),
40 'refmap': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
41 'text': ('markupfield.fields.MarkupField', [], {'rendered_field': 'True'}),
42 'text_markup_type': ('django.db.models.fields.CharField', [], {'default': "'bbcode'", 'max_length': '30'}),
43 'thread_new': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.Thread']", 'null': 'True'}),
44 'title': ('django.db.models.fields.CharField', [], {'max_length': '200'})
45 },
46 'boards.postimage': {
47 'Meta': {'ordering': "('id',)", 'object_name': 'PostImage'},
48 'hash': ('django.db.models.fields.CharField', [], {'max_length': '36'}),
49 'height': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
50 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
51 'image': ('boards.thumbs.ImageWithThumbsField', [], {'max_length': '100', 'blank': 'True'}),
52 'pre_height': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
53 'pre_width': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
54 'width': ('django.db.models.fields.IntegerField', [], {'default': '0'})
55 },
56 'boards.tag': {
57 'Meta': {'ordering': "('name',)", 'object_name': 'Tag'},
58 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
59 'name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}),
60 'threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'tag+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Thread']"})
61 },
62 'boards.thread': {
63 'Meta': {'object_name': 'Thread'},
64 'archived': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
65 'bump_time': ('django.db.models.fields.DateTimeField', [], {}),
66 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
67 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}),
68 'replies': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'tre+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
69 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['boards.Tag']", 'symmetrical': 'False'})
70 }
71 }
72
73 complete_apps = ['boards'] No newline at end of file
@@ -0,0 +1,24 b''
1 from django.shortcuts import render
2
3 from boards.abstracts.settingsmanager import PERMISSION_MODERATE,\
4 get_settings_manager
5 from boards.forms import LoginForm
6 from boards.views.base import BaseBoardView, CONTEXT_FORM
7
8
9 __author__ = 'neko259'
10
11
12 class LogoutView(BaseBoardView):
13
14 def get(self, request, form=None):
15 settings_manager = get_settings_manager(request)
16 settings_manager.del_permission(PERMISSION_MODERATE)
17
18 context = self.get_context_data(request=request)
19
20 if not form:
21 form = LoginForm()
22 context[CONTEXT_FORM] = form
23
24 return render(request, 'boards/login.html', context) No newline at end of file
@@ -1,40 +1,31 b''
1 1 from django.contrib import admin
2 from boards.models import Post, Tag, User, Ban, Thread
2 from boards.models import Post, Tag, Ban, Thread
3 3
4 4
5 5 class PostAdmin(admin.ModelAdmin):
6 6
7 7 list_display = ('id', 'title', 'text')
8 8 list_filter = ('pub_time', 'thread_new')
9 9 search_fields = ('id', 'title', 'text')
10 10
11 11
12 12 class TagAdmin(admin.ModelAdmin):
13 13
14 list_display = ('name', 'linked')
15 list_filter = ('linked',)
16
17
18 class UserAdmin(admin.ModelAdmin):
19
20 list_display = ('user_id', 'rank')
21 search_fields = ('user_id',)
22
14 list_display = ('name',)
23 15
24 16 class ThreadAdmin(admin.ModelAdmin):
25 17
26 18 def title(self, obj):
27 19 return obj.get_opening_post().title
28 20
29 21 def reply_count(self, obj):
30 22 return obj.get_reply_count()
31 23
32 24 list_display = ('id', 'title', 'reply_count', 'archived')
33 25 list_filter = ('bump_time', 'archived')
34 26 search_fields = ('id', 'title')
35 27
36 28 admin.site.register(Post, PostAdmin)
37 29 admin.site.register(Tag, TagAdmin)
38 admin.site.register(User, UserAdmin)
39 30 admin.site.register(Ban)
40 31 admin.site.register(Thread, ThreadAdmin)
@@ -1,39 +1,37 b''
1 from boards.abstracts.settingsmanager import PERMISSION_MODERATE, \
2 get_settings_manager
3
1 4 __author__ = 'neko259'
2 5
3 from boards import utils, settings
6 from boards import settings
4 7 from boards.models import Post
5 from boards.models.post import SETTING_MODERATE
6 8
7 9 CONTEXT_SITE_NAME = 'site_name'
8 10 CONTEXT_VERSION = 'version'
9 11 CONTEXT_MODERATOR = 'moderator'
10 12 CONTEXT_THEME_CSS = 'theme_css'
11 13 CONTEXT_THEME = 'theme'
12 14 CONTEXT_PPD = 'posts_per_day'
13 15 CONTEXT_TAGS = 'tags'
14 16 CONTEXT_USER = 'user'
15 17
16 18
17 19 def user_and_ui_processor(request):
18 20 context = {}
19 21
20 user = utils.get_user(request)
21 context[CONTEXT_USER] = user
22 context[CONTEXT_TAGS] = user.fav_tags.all()
23 22 context[CONTEXT_PPD] = float(Post.objects.get_posts_per_day())
24 23
25 theme = utils.get_theme(request, user)
24 settings_manager = get_settings_manager(request)
25 context[CONTEXT_TAGS] = settings_manager.get_fav_tags()
26 theme = settings_manager.get_theme()
26 27 context[CONTEXT_THEME] = theme
27 28 context[CONTEXT_THEME_CSS] = 'css/' + theme + '/base_page.css'
28 29
29 30 # This shows the moderator panel
30 moderate = user.get_setting(SETTING_MODERATE)
31 if moderate == 'True':
32 context[CONTEXT_MODERATOR] = user.is_moderator()
33 else:
34 context[CONTEXT_MODERATOR] = False
31 moderate = settings_manager.has_permission(PERMISSION_MODERATE)
32 context[CONTEXT_MODERATOR] = moderate
35 33
36 34 context[CONTEXT_VERSION] = settings.VERSION
37 35 context[CONTEXT_SITE_NAME] = settings.SITE_NAME
38 36
39 37 return context No newline at end of file
@@ -1,357 +1,341 b''
1 1 import re
2 2 import time
3 3 import hashlib
4 4
5 5 from captcha.fields import CaptchaField
6 6 from django import forms
7 7 from django.forms.util import ErrorList
8 8 from django.utils.translation import ugettext_lazy as _
9 9
10 10 from boards.mdx_neboard import formatters
11 11 from boards.models.post import TITLE_MAX_LENGTH
12 from boards.models import User, PostImage
12 from boards.models import PostImage
13 13 from neboard import settings
14 14 from boards import utils
15 15 import boards.settings as board_settings
16 16
17 17 VETERAN_POSTING_DELAY = 5
18 18
19 19 ATTRIBUTE_PLACEHOLDER = 'placeholder'
20 20
21 21 LAST_POST_TIME = 'last_post_time'
22 22 LAST_LOGIN_TIME = 'last_login_time'
23 TEXT_PLACEHOLDER = _('''Type message here. You can reply to message >>123 like
24 this. 2 new lines are required to start new paragraph.''')
23 TEXT_PLACEHOLDER = _('''Type message here. Use formatting panel for more advanced usage.''')
25 24 TAGS_PLACEHOLDER = _('tag1 several_words_tag')
26 25
27 26 ERROR_IMAGE_DUPLICATE = _('Such image was already posted')
28 27
29 28 LABEL_TITLE = _('Title')
30 29 LABEL_TEXT = _('Text')
31 30 LABEL_TAG = _('Tag')
32 31 LABEL_SEARCH = _('Search')
33 32
34 33 TAG_MAX_LENGTH = 20
35 34
36 35 REGEX_TAG = ur'^[\w\d]+$'
37 36
38 37
39 38 class FormatPanel(forms.Textarea):
40 39 def render(self, name, value, attrs=None):
41 40 output = '<div id="mark-panel">'
42 41 for formatter in formatters:
43 42 output += u'<span class="mark_btn"' + \
44 43 u' onClick="addMarkToMsg(\'' + formatter.format_left + \
45 44 '\', \'' + formatter.format_right + '\')">' + \
46 45 formatter.preview_left + formatter.name + \
47 46 formatter.preview_right + u'</span>'
48 47
49 48 output += '</div>'
50 49 output += super(FormatPanel, self).render(name, value, attrs=None)
51 50
52 51 return output
53 52
54 53
55 54 class PlainErrorList(ErrorList):
56 55 def __unicode__(self):
57 56 return self.as_text()
58 57
59 58 def as_text(self):
60 59 return ''.join([u'(!) %s ' % e for e in self])
61 60
62 61
63 62 class NeboardForm(forms.Form):
64 63
65 64 def as_div(self):
66 65 """
67 66 Returns this form rendered as HTML <as_div>s.
68 67 """
69 68
70 69 return self._html_output(
71 70 # TODO Do not show hidden rows in the list here
72 71 normal_row='<div class="form-row"><div class="form-label">'
73 72 '%(label)s'
74 73 '</div></div>'
75 74 '<div class="form-row"><div class="form-input">'
76 75 '%(field)s'
77 76 '</div></div>'
78 77 '<div class="form-row">'
79 78 '%(help_text)s'
80 79 '</div>',
81 80 error_row='<div class="form-row">'
82 81 '<div class="form-label"></div>'
83 82 '<div class="form-errors">%s</div>'
84 83 '</div>',
85 84 row_ender='</div>',
86 85 help_text_html='%s',
87 86 errors_on_separate_row=True)
88 87
89 88 def as_json_errors(self):
90 89 errors = []
91 90
92 91 for name, field in self.fields.items():
93 92 if self[name].errors:
94 93 errors.append({
95 94 'field': name,
96 95 'errors': self[name].errors.as_text(),
97 96 })
98 97
99 98 return errors
100 99
101 100
102 101 class PostForm(NeboardForm):
103 102
104 103 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
105 104 label=LABEL_TITLE)
106 105 text = forms.CharField(
107 106 widget=FormatPanel(attrs={ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER}),
108 107 required=False, label=LABEL_TEXT)
109 108 image = forms.ImageField(required=False, label=_('Image'),
110 109 widget=forms.ClearableFileInput(
111 110 attrs={'accept': 'image/*'}))
112 111
113 112 # This field is for spam prevention only
114 113 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
115 114 widget=forms.TextInput(attrs={
116 115 'class': 'form-email'}))
117 116
118 117 session = None
119 118 need_to_ban = False
120 119
121 120 def clean_title(self):
122 121 title = self.cleaned_data['title']
123 122 if title:
124 123 if len(title) > TITLE_MAX_LENGTH:
125 124 raise forms.ValidationError(_('Title must have less than %s '
126 125 'characters') %
127 126 str(TITLE_MAX_LENGTH))
128 127 return title
129 128
130 129 def clean_text(self):
131 130 text = self.cleaned_data['text'].strip()
132 131 if text:
133 132 if len(text) > board_settings.MAX_TEXT_LENGTH:
134 133 raise forms.ValidationError(_('Text must have less than %s '
135 134 'characters') %
136 135 str(board_settings
137 136 .MAX_TEXT_LENGTH))
138 137 return text
139 138
140 139 def clean_image(self):
141 140 image = self.cleaned_data['image']
142 141 if image:
143 142 if image.size > board_settings.MAX_IMAGE_SIZE:
144 143 raise forms.ValidationError(
145 144 _('Image must be less than %s bytes')
146 145 % str(board_settings.MAX_IMAGE_SIZE))
147 146
148 147 md5 = hashlib.md5()
149 148 for chunk in image.chunks():
150 149 md5.update(chunk)
151 150 image_hash = md5.hexdigest()
152 151 if PostImage.objects.filter(hash=image_hash).exists():
153 152 raise forms.ValidationError(ERROR_IMAGE_DUPLICATE)
154 153
155 154 return image
156 155
157 156 def clean(self):
158 157 cleaned_data = super(PostForm, self).clean()
159 158
160 159 if not self.session:
161 160 raise forms.ValidationError('Humans have sessions')
162 161
163 162 if cleaned_data['email']:
164 163 self.need_to_ban = True
165 164 raise forms.ValidationError('A human cannot enter a hidden field')
166 165
167 166 if not self.errors:
168 167 self._clean_text_image()
169 168
170 169 if not self.errors and self.session:
171 170 self._validate_posting_speed()
172 171
173 172 return cleaned_data
174 173
175 174 def _clean_text_image(self):
176 175 text = self.cleaned_data.get('text')
177 176 image = self.cleaned_data.get('image')
178 177
179 178 if (not text) and (not image):
180 179 error_message = _('Either text or image must be entered.')
181 180 self._errors['text'] = self.error_class([error_message])
182 181
183 182 def _validate_posting_speed(self):
184 183 can_post = True
185 184
186 185 # TODO Remove this, it's only for test
187 186 if not 'user_id' in self.session:
188 187 return
189 188
190 user = User.objects.get(id=self.session['user_id'])
191 if user.is_veteran():
192 posting_delay = VETERAN_POSTING_DELAY
193 else:
194 189 posting_delay = settings.POSTING_DELAY
195 190
196 191 if board_settings.LIMIT_POSTING_SPEED and LAST_POST_TIME in \
197 192 self.session:
198 193 now = time.time()
199 194 last_post_time = self.session[LAST_POST_TIME]
200 195
201 196 current_delay = int(now - last_post_time)
202 197
203 198 if current_delay < posting_delay:
204 199 error_message = _('Wait %s seconds after last posting') % str(
205 200 posting_delay - current_delay)
206 201 self._errors['text'] = self.error_class([error_message])
207 202
208 203 can_post = False
209 204
210 205 if can_post:
211 206 self.session[LAST_POST_TIME] = time.time()
212 207
213 208
214 209 class ThreadForm(PostForm):
215 210
216 211 regex_tags = re.compile(ur'^[\w\s\d]+$', re.UNICODE)
217 212
218 213 tags = forms.CharField(
219 214 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
220 215 max_length=100, label=_('Tags'), required=True)
221 216
222 217 def clean_tags(self):
223 218 tags = self.cleaned_data['tags'].strip()
224 219
225 220 if not tags or not self.regex_tags.match(tags):
226 221 raise forms.ValidationError(
227 222 _('Inappropriate characters in tags.'))
228 223
229 224 return tags
230 225
231 226 def clean(self):
232 227 cleaned_data = super(ThreadForm, self).clean()
233 228
234 229 return cleaned_data
235 230
236 231
237 232 class PostCaptchaForm(PostForm):
238 233 captcha = CaptchaField()
239 234
240 235 def __init__(self, *args, **kwargs):
241 236 self.request = kwargs['request']
242 237 del kwargs['request']
243 238
244 239 super(PostCaptchaForm, self).__init__(*args, **kwargs)
245 240
246 241 def clean(self):
247 242 cleaned_data = super(PostCaptchaForm, self).clean()
248 243
249 244 success = self.is_valid()
250 245 utils.update_captcha_access(self.request, success)
251 246
252 247 if success:
253 248 return cleaned_data
254 249 else:
255 250 raise forms.ValidationError(_("Captcha validation failed"))
256 251
257 252
258 253 class ThreadCaptchaForm(ThreadForm):
259 254 captcha = CaptchaField()
260 255
261 256 def __init__(self, *args, **kwargs):
262 257 self.request = kwargs['request']
263 258 del kwargs['request']
264 259
265 260 super(ThreadCaptchaForm, self).__init__(*args, **kwargs)
266 261
267 262 def clean(self):
268 263 cleaned_data = super(ThreadCaptchaForm, self).clean()
269 264
270 265 success = self.is_valid()
271 266 utils.update_captcha_access(self.request, success)
272 267
273 268 if success:
274 269 return cleaned_data
275 270 else:
276 271 raise forms.ValidationError(_("Captcha validation failed"))
277 272
278 273
279 274 class SettingsForm(NeboardForm):
280 275
281 276 theme = forms.ChoiceField(choices=settings.THEMES,
282 277 label=_('Theme'))
283 278
284 279
285 class ModeratorSettingsForm(SettingsForm):
286
287 moderate = forms.BooleanField(required=False, label=_('Enable moderation '
288 'panel'))
289
290
291 class LoginForm(NeboardForm):
292
293 user_id = forms.CharField()
294
295 session = None
296
297 def clean_user_id(self):
298 user_id = self.cleaned_data['user_id']
299 if user_id:
300 users = User.objects.filter(user_id=user_id)
301 if len(users) == 0:
302 raise forms.ValidationError(_('No such user found'))
303
304 return user_id
305
306 def _validate_login_speed(self):
307 can_post = True
308
309 if LAST_LOGIN_TIME in self.session:
310 now = time.time()
311 last_login_time = self.session[LAST_LOGIN_TIME]
312
313 current_delay = int(now - last_login_time)
314
315 if current_delay < board_settings.LOGIN_TIMEOUT:
316 error_message = _('Wait %s minutes after last login') % str(
317 (board_settings.LOGIN_TIMEOUT - current_delay) / 60)
318 self._errors['user_id'] = self.error_class([error_message])
319
320 can_post = False
321
322 if can_post:
323 self.session[LAST_LOGIN_TIME] = time.time()
324
325 def clean(self):
326 if not self.session:
327 raise forms.ValidationError('Humans have sessions')
328
329 self._validate_login_speed()
330
331 cleaned_data = super(LoginForm, self).clean()
332
333 return cleaned_data
334
335
336 280 class AddTagForm(NeboardForm):
337 281
338 282 tag = forms.CharField(max_length=TAG_MAX_LENGTH, label=LABEL_TAG)
339 283 method = forms.CharField(widget=forms.HiddenInput(), initial='add_tag')
340 284
341 285 def clean_tag(self):
342 286 tag = self.cleaned_data['tag']
343 287
344 288 regex_tag = re.compile(REGEX_TAG, re.UNICODE)
345 289 if not regex_tag.match(tag):
346 290 raise forms.ValidationError(_('Inappropriate characters in tags.'))
347 291
348 292 return tag
349 293
350 294 def clean(self):
351 295 cleaned_data = super(AddTagForm, self).clean()
352 296
353 297 return cleaned_data
354 298
355 299
356 300 class SearchForm(NeboardForm):
357 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False) No newline at end of file
301 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
302
303
304 class LoginForm(NeboardForm):
305
306 password = forms.CharField()
307
308 session = None
309
310 def clean_password(self):
311 password = self.cleaned_data['password']
312 if board_settings.MASTER_PASSWORD != password:
313 raise forms.ValidationError(_('Invalid master password'))
314
315 return password
316
317 def _validate_login_speed(self):
318 can_post = True
319
320 if LAST_LOGIN_TIME in self.session:
321 now = time.time()
322 last_login_time = self.session[LAST_LOGIN_TIME]
323
324 current_delay = int(now - last_login_time)
325
326 if current_delay < board_settings.LOGIN_TIMEOUT:
327 error_message = _('Wait %s minutes after last login') % str(
328 (board_settings.LOGIN_TIMEOUT - current_delay) / 60)
329 self._errors['password'] = self.error_class([error_message])
330
331 can_post = False
332
333 if can_post:
334 self.session[LAST_LOGIN_TIME] = time.time()
335
336 def clean(self):
337 self._validate_login_speed()
338
339 cleaned_data = super(LoginForm, self).clean()
340
341 return cleaned_data
1 NO CONTENT: modified file, binary diff hidden
@@ -1,392 +1,361 b''
1 1 # SOME DESCRIPTIVE TITLE.
2 2 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3 3 # This file is distributed under the same license as the PACKAGE package.
4 4 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
5 5 #
6 6 msgid ""
7 7 msgstr ""
8 8 "Project-Id-Version: PACKAGE VERSION\n"
9 9 "Report-Msgid-Bugs-To: \n"
10 "POT-Creation-Date: 2014-06-29 13:46+0300\n"
10 "POT-Creation-Date: 2014-07-20 20:11+0300\n"
11 11 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
12 12 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
13 13 "Language-Team: LANGUAGE <LL@li.org>\n"
14 14 "Language: ru\n"
15 15 "MIME-Version: 1.0\n"
16 16 "Content-Type: text/plain; charset=UTF-8\n"
17 17 "Content-Transfer-Encoding: 8bit\n"
18 18 "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
19 19 "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
20 20
21 #: authors.py:5
21 #: authors.py:9
22 22 msgid "author"
23 23 msgstr "автор"
24 24
25 #: authors.py:6
25 #: authors.py:10
26 26 msgid "developer"
27 27 msgstr "разработчик"
28 28
29 #: authors.py:7
29 #: authors.py:11
30 30 msgid "javascript developer"
31 31 msgstr "разработчик javascript"
32 32
33 #: authors.py:8
33 #: authors.py:12
34 34 msgid "designer"
35 35 msgstr "дизайнер"
36 36
37 37 #: forms.py:23
38 msgid ""
39 "Type message here. You can reply to message >>123 like\n"
40 " this. 2 new lines are required to start new paragraph."
38 msgid "Type message here. Use formatting panel for more advanced usage."
41 39 msgstr ""
42 "Введите сообщение здесь. Вы можете ответить на сообщение >>123 вот так. 2 "
43 "переноса строки обязательны для создания нового абзаца."
40 "Вводите сообщение сюда. Используйте панель для более сложного форматирования."
44 41
45 #: forms.py:25
42 #: forms.py:24
46 43 msgid "tag1 several_words_tag"
47 44 msgstr "тег1 тег_из_нескольких_слов"
48 45
49 #: forms.py:27
46 #: forms.py:26
50 47 msgid "Such image was already posted"
51 48 msgstr "Такое изображение уже было загружено"
52 49
53 #: forms.py:29
50 #: forms.py:28
54 51 msgid "Title"
55 52 msgstr "Заголовок"
56 53
57 #: forms.py:30
54 #: forms.py:29
58 55 msgid "Text"
59 56 msgstr "Текст"
60 57
61 #: forms.py:31
58 #: forms.py:30
62 59 msgid "Tag"
63 60 msgstr "Тег"
64 61
65 #: forms.py:32 templates/boards/base.html:50 templates/search/search.html:9
62 #: forms.py:31 templates/boards/base.html:54 templates/search/search.html:9
66 63 #: templates/search/search.html.py:13
67 64 msgid "Search"
68 65 msgstr "Поиск"
69 66
70 #: forms.py:109
67 #: forms.py:108
71 68 msgid "Image"
72 69 msgstr "Изображение"
73 70
74 71 #: forms.py:113
75 72 msgid "e-mail"
76 73 msgstr ""
77 74
78 75 #: forms.py:124
79 76 #, python-format
80 77 msgid "Title must have less than %s characters"
81 78 msgstr "Заголовок должен иметь меньше %s символов"
82 79
83 80 #: forms.py:133
84 81 #, python-format
85 82 msgid "Text must have less than %s characters"
86 83 msgstr "Текст должен быть короче %s символов"
87 84
88 85 #: forms.py:144
89 86 #, python-format
90 87 msgid "Image must be less than %s bytes"
91 88 msgstr "Изображение должно быть менее %s байт"
92 89
93 90 #: forms.py:179
94 91 msgid "Either text or image must be entered."
95 92 msgstr "Текст или картинка должны быть введены."
96 93
97 #: forms.py:202
94 #: forms.py:199
98 95 #, python-format
99 96 msgid "Wait %s seconds after last posting"
100 97 msgstr "Подождите %s секунд после последнего постинга"
101 98
102 #: forms.py:218 templates/boards/tags.html:7 templates/boards/rss/post.html:10
99 #: forms.py:215 templates/boards/tags.html:7 templates/boards/rss/post.html:10
103 100 msgid "Tags"
104 101 msgstr "Теги"
105 102
106 #: forms.py:225 forms.py:344
103 #: forms.py:222 forms.py:290
107 104 msgid "Inappropriate characters in tags."
108 105 msgstr "Недопустимые символы в тегах."
109 106
110 #: forms.py:253 forms.py:274
107 #: forms.py:250 forms.py:271
111 108 msgid "Captcha validation failed"
112 109 msgstr "Проверка капчи провалена"
113 110
114 #: forms.py:280
111 #: forms.py:277
115 112 msgid "Theme"
116 113 msgstr "Тема"
117 114
118 #: forms.py:285
119 msgid "Enable moderation panel"
120 msgstr "Включить панель модерации"
115 #: forms.py:313
116 msgid "Invalid master password"
117 msgstr "Неверный мастер-пароль"
121 118
122 #: forms.py:300
123 msgid "No such user found"
124 msgstr "Данный пользователь не найден"
125
126 #: forms.py:314
119 #: forms.py:327
127 120 #, python-format
128 121 msgid "Wait %s minutes after last login"
129 122 msgstr "Подождите %s минут после последнего входа"
130 123
131 124 #: templates/boards/404.html:6
132 125 msgid "Not found"
133 126 msgstr "Не найдено"
134 127
135 128 #: templates/boards/404.html:12
136 129 msgid "This page does not exist"
137 130 msgstr "Этой страницы не существует"
138 131
139 132 #: templates/boards/authors.html:6 templates/boards/authors.html.py:12
140 133 msgid "Authors"
141 134 msgstr "Авторы"
142 135
143 136 #: templates/boards/authors.html:26
144 137 msgid "Distributed under the"
145 138 msgstr "Распространяется под"
146 139
147 140 #: templates/boards/authors.html:28
148 141 msgid "license"
149 142 msgstr "лицензией"
150 143
151 144 #: templates/boards/authors.html:30
152 145 msgid "Repository"
153 146 msgstr "Репозиторий"
154 147
155 148 #: templates/boards/base.html:12
156 149 msgid "Feed"
157 150 msgstr "Лента"
158 151
159 152 #: templates/boards/base.html:29
160 153 msgid "All threads"
161 154 msgstr "Все темы"
162 155
163 156 #: templates/boards/base.html:34
164 157 msgid "Tag management"
165 158 msgstr "Управление тегами"
166 159
167 160 #: templates/boards/base.html:36 templates/boards/settings.html:7
168 161 msgid "Settings"
169 162 msgstr "Настройки"
170 163
171 #: templates/boards/base.html:49 templates/boards/login.html:6
164 #: templates/boards/base.html:50
165 msgid "Logout"
166 msgstr "Выход"
167
168 #: templates/boards/base.html:52 templates/boards/login.html:6
172 169 #: templates/boards/login.html.py:16
173 170 msgid "Login"
174 171 msgstr "Вход"
175 172
176 #: templates/boards/base.html:52
173 #: templates/boards/base.html:56
177 174 #, python-format
178 175 msgid "Speed: %(ppd)s posts per day"
179 176 msgstr "Скорость: %(ppd)s сообщений в день"
180 177
181 #: templates/boards/base.html:54
178 #: templates/boards/base.html:58
182 179 msgid "Up"
183 180 msgstr "Вверх"
184 181
185 182 #: templates/boards/login.html:19
186 183 msgid "Insert your user id above"
187 184 msgstr "Вставьте свой ID пользователя выше"
188 185
189 #: templates/boards/post.html:21 templates/boards/staticpages/help.html:19
186 #: templates/boards/post.html:21 templates/boards/staticpages/help.html:17
190 187 msgid "Quote"
191 188 msgstr "Цитата"
192 189
193 190 #: templates/boards/post.html:31
194 191 msgid "Open"
195 192 msgstr "Открыть"
196 193
197 194 #: templates/boards/post.html:33
198 195 msgid "Reply"
199 196 msgstr "Ответ"
200 197
201 198 #: templates/boards/post.html:40
202 199 msgid "Edit"
203 200 msgstr "Изменить"
204 201
205 202 #: templates/boards/post.html:42
206 203 msgid "Delete"
207 204 msgstr "Удалить"
208 205
209 206 #: templates/boards/post.html:45
210 207 msgid "Ban IP"
211 208 msgstr "Заблокировать IP"
212 209
213 210 #: templates/boards/post.html:76
214 211 msgid "Replies"
215 212 msgstr "Ответы"
216 213
217 214 #: templates/boards/post.html:86 templates/boards/thread.html:88
218 215 #: templates/boards/thread_gallery.html:61
219 msgid "replies"
220 msgstr "ответов"
216 msgid "messages"
217 msgstr "сообщений"
221 218
222 219 #: templates/boards/post.html:87 templates/boards/thread.html:89
223 220 #: templates/boards/thread_gallery.html:62
224 221 msgid "images"
225 222 msgstr "изображений"
226 223
227 224 #: templates/boards/post_admin.html:19
228 225 msgid "Tags:"
229 226 msgstr "Теги:"
230 227
231 228 #: templates/boards/post_admin.html:30
232 229 msgid "Add tag"
233 230 msgstr "Добавить тег"
234 231
235 232 #: templates/boards/posting_general.html:56
236 233 msgid "Show tag"
237 234 msgstr "Показывать тег"
238 235
239 236 #: templates/boards/posting_general.html:60
240 237 msgid "Hide tag"
241 238 msgstr "Скрывать тег"
242 239
243 240 #: templates/boards/posting_general.html:79 templates/search/search.html:22
244 241 msgid "Previous page"
245 242 msgstr "Предыдущая страница"
246 243
247 244 #: templates/boards/posting_general.html:94
248 245 #, python-format
249 246 msgid "Skipped %(count)s replies. Open thread to see all replies."
250 247 msgstr "Пропущено %(count)s ответов. Откройте тред, чтобы увидеть все ответы."
251 248
252 #: templates/boards/posting_general.html:121 templates/search/search.html:35
249 #: templates/boards/posting_general.html:121 templates/search/search.html:33
253 250 msgid "Next page"
254 251 msgstr "Следующая страница"
255 252
256 253 #: templates/boards/posting_general.html:126
257 254 msgid "No threads exist. Create the first one!"
258 255 msgstr "Нет тем. Создайте первую!"
259 256
260 257 #: templates/boards/posting_general.html:132
261 258 msgid "Create new thread"
262 259 msgstr "Создать новую тему"
263 260
264 261 #: templates/boards/posting_general.html:137 templates/boards/thread.html:58
265 262 msgid "Post"
266 263 msgstr "Отправить"
267 264
268 265 #: templates/boards/posting_general.html:142
269 266 msgid "Tags must be delimited by spaces. Text or image is required."
270 267 msgstr ""
271 268 "Теги должны быть разделены пробелами. Текст или изображение обязательны."
272 269
273 270 #: templates/boards/posting_general.html:145 templates/boards/thread.html:66
274 271 msgid "Text syntax"
275 272 msgstr "Синтаксис текста"
276 273
277 274 #: templates/boards/posting_general.html:157
278 275 msgid "Pages:"
279 276 msgstr "Страницы: "
280 277
281 #: templates/boards/settings.html:14
282 msgid "User:"
283 msgstr "Пользователь:"
284
285 #: templates/boards/settings.html:16
278 #: templates/boards/settings.html:15
286 279 msgid "You are moderator."
287 280 msgstr "Вы модератор."
288 281
289 282 #: templates/boards/settings.html:19
290 msgid "You are veteran."
291 msgstr "Вы ветеран."
292
293 #: templates/boards/settings.html:22
294 msgid "Posts:"
295 msgstr "Сообщений:"
296
297 #: templates/boards/settings.html:23
298 msgid "First access:"
299 msgstr "Первый доступ:"
300
301 #: templates/boards/settings.html:25
302 msgid "Last access:"
303 msgstr "Последний доступ: "
304
305 #: templates/boards/settings.html:29
306 283 msgid "Hidden tags:"
307 284 msgstr "Скрытые теги:"
308 285
309 #: templates/boards/settings.html:44
286 #: templates/boards/settings.html:26
287 msgid "No hidden tags."
288 msgstr "Нет скрытых тегов."
289
290 #: templates/boards/settings.html:35
310 291 msgid "Save"
311 292 msgstr "Сохранить"
312 293
313 294 #: templates/boards/tags.html:22
314 295 msgid "No tags found."
315 296 msgstr "Теги не найдены."
316 297
317 298 #: templates/boards/thread.html:20 templates/boards/thread_gallery.html:21
318 299 msgid "Normal mode"
319 300 msgstr "Нормальный режим"
320 301
321 302 #: templates/boards/thread.html:21 templates/boards/thread_gallery.html:22
322 303 msgid "Gallery mode"
323 304 msgstr "Режим галереи"
324 305
325 306 #: templates/boards/thread.html:29
326 307 msgid "posts to bumplimit"
327 308 msgstr "сообщений до бамплимита"
328 309
329 310 #: templates/boards/thread.html:50
330 311 msgid "Reply to thread"
331 312 msgstr "Ответить в тему"
332 313
333 314 #: templates/boards/thread.html:63
334 315 msgid "Switch mode"
335 316 msgstr "Переключить режим"
336 317
337 318 #: templates/boards/thread.html:90 templates/boards/thread_gallery.html:63
338 319 msgid "Last update: "
339 320 msgstr "Последнее обновление: "
340 321
341 322 #: templates/boards/rss/post.html:5
342 323 msgid "Post image"
343 324 msgstr "Изображение сообщения"
344 325
345 326 #: templates/boards/staticpages/banned.html:6
346 327 msgid "Banned"
347 328 msgstr "Заблокирован"
348 329
349 330 #: templates/boards/staticpages/banned.html:11
350 331 msgid "Your IP address has been banned. Contact the administrator"
351 332 msgstr "Ваш IP адрес был заблокирован. Свяжитесь с администратором"
352 333
353 334 #: templates/boards/staticpages/help.html:6
354 335 #: templates/boards/staticpages/help.html:10
355 336 msgid "Syntax"
356 337 msgstr "Синтаксис"
357 338
358 339 #: templates/boards/staticpages/help.html:11
359 msgid "2 line breaks for a new line."
360 msgstr "2 перевода строки создают новый абзац."
361
362 #: templates/boards/staticpages/help.html:12
363 340 msgid "Italic text"
364 341 msgstr "Курсивный текст"
365 342
366 #: templates/boards/staticpages/help.html:13
343 #: templates/boards/staticpages/help.html:12
367 344 msgid "Bold text"
368 345 msgstr "Полужирный текст"
369 346
370 #: templates/boards/staticpages/help.html:14
347 #: templates/boards/staticpages/help.html:13
371 348 msgid "Spoiler"
372 349 msgstr "Спойлер"
373 350
374 #: templates/boards/staticpages/help.html:15
351 #: templates/boards/staticpages/help.html:14
375 352 msgid "Link to a post"
376 353 msgstr "Ссылка на сообщение"
377 354
378 #: templates/boards/staticpages/help.html:16
355 #: templates/boards/staticpages/help.html:15
379 356 msgid "Strikethrough text"
380 357 msgstr "Зачеркнутый текст"
381 358
382 #: templates/boards/staticpages/help.html:17
383 msgid "You need to new line before:"
384 msgstr "Перед этими тегами нужна новая строка:"
385
386 #: templates/boards/staticpages/help.html:18
359 #: templates/boards/staticpages/help.html:16
387 360 msgid "Comment"
388 361 msgstr "Комментарий"
389
390 #: templates/search/search.html:30
391 msgid "No results found."
392 msgstr "Результаты не найдены."
@@ -1,16 +1,16 b''
1 1 from django.core.management import BaseCommand
2 2 from django.db import transaction
3 3
4 4 from boards.models import Post
5 5 from boards.models.post import NO_IP
6 6
7 7
8 8 __author__ = 'neko259'
9 9
10 10
11 11 class Command(BaseCommand):
12 12 help = 'Removes user and IP data from all posts'
13 13
14 14 @transaction.atomic
15 15 def handle(self, *args, **options):
16 Post.objects.all().update(poster_ip=NO_IP, user=None) No newline at end of file
16 Post.objects.all().update(poster_ip=NO_IP) No newline at end of file
@@ -1,207 +1,152 b''
1 1 # coding=utf-8
2 2
3 import markdown
4 from markdown.inlinepatterns import Pattern
5 from markdown.util import etree
3 import re
4 import bbcode
6 5
7 6 import boards
8 7
9 8
10 9 __author__ = 'neko259'
11 10
12 11
13 AUTOLINK_PATTERN = r'(https?://\S+)'
14 QUOTE_PATTERN = r'^(?<!>)(>[^>].*)$'
15 REFLINK_PATTERN = r'((>>)(\d+))'
16 SPOILER_PATTERN = r'%%([^(%%)]+)%%'
17 COMMENT_PATTERN = r'^(//(.+))'
18 STRIKETHROUGH_PATTERN = r'~(.+)~'
19 DASH_PATTERN = r'--'
12 REFLINK_PATTERN = re.compile(r'\d+')
20 13
21 14
22 15 class TextFormatter():
23 16 """
24 17 An interface for formatter that can be used in the text format panel
25 18 """
26 19
27 20 def __init__(self):
28 21 pass
29 22
30 23 name = ''
31 24
32 25 # Left and right tags for the button preview
33 26 preview_left = ''
34 27 preview_right = ''
35 28
36 29 # Left and right characters for the textarea input
37 30 format_left = ''
38 31 format_right = ''
39 32
40 33
41 class AutolinkPattern(Pattern):
34 class AutolinkPattern():
42 35 def handleMatch(self, m):
43 36 link_element = etree.Element('a')
44 37 href = m.group(2)
45 38 link_element.set('href', href)
46 39 link_element.text = href
47 40
48 41 return link_element
49 42
50 43
51 class QuotePattern(Pattern, TextFormatter):
52 name = ''
53 preview_left = '<span class="quote">&gt; '
44 class QuotePattern(TextFormatter):
45 name = 'q'
46 preview_left = '<span class="multiquote">'
54 47 preview_right = '</span>'
55 48
56 format_left = '&gt;'
57
58 def handleMatch(self, m):
59 quote_element = etree.Element('span')
60 quote_element.set('class', 'quote')
61 quote_element.text = m.group(2)
62
63 return quote_element
49 format_left = '[quote]'
50 format_right = '[/quote]'
64 51
65 52
66 class ReflinkPattern(Pattern):
67 def handleMatch(self, m):
68 post_id = m.group(4)
69
70 posts = boards.models.Post.objects.filter(id=post_id)
71 if posts.count() > 0:
72 ref_element = etree.Element('a')
73
74 post = posts[0]
75
76 ref_element.set('href', post.get_url())
77 ref_element.text = m.group(2)
78
79 return ref_element
80
81
82 class SpoilerPattern(Pattern, TextFormatter):
83 name = 's'
53 class SpoilerPattern(TextFormatter):
54 name = 'spoiler'
84 55 preview_left = '<span class="spoiler">'
85 56 preview_right = '</span>'
86 57
87 format_left = '%%'
88 format_right = '%%'
58 format_left = '[spoiler]'
59 format_right = '[/spoiler]'
89 60
90 61 def handleMatch(self, m):
91 62 quote_element = etree.Element('span')
92 63 quote_element.set('class', 'spoiler')
93 64 quote_element.text = m.group(2)
94 65
95 66 return quote_element
96 67
97 68
98 class CommentPattern(Pattern, TextFormatter):
69 class CommentPattern(TextFormatter):
99 70 name = ''
100 71 preview_left = '<span class="comment">// '
101 72 preview_right = '</span>'
102 73
103 format_left = '//'
104
105 def handleMatch(self, m):
106 quote_element = etree.Element('span')
107 quote_element.set('class', 'comment')
108 quote_element.text = '//' + m.group(3)
109
110 return quote_element
74 format_left = '[comment]'
75 format_right = '[/comment]'
111 76
112 77
113 class StrikeThroughPattern(Pattern, TextFormatter):
78 class StrikeThroughPattern(TextFormatter):
114 79 name = 's'
115 80 preview_left = '<span class="strikethrough">'
116 81 preview_right = '</span>'
117 82
118 format_left = '~'
119 format_right = '~'
120
121 def handleMatch(self, m):
122 quote_element = etree.Element('span')
123 quote_element.set('class', 'strikethrough')
124 quote_element.text = m.group(2)
125
126 return quote_element
83 format_left = '[s]'
84 format_right = '[/s]'
127 85
128 86
129 87 class ItalicPattern(TextFormatter):
130 88 name = 'i'
131 89 preview_left = '<i>'
132 90 preview_right = '</i>'
133 91
134 format_left = '_'
135 format_right = '_'
92 format_left = '[i]'
93 format_right = '[/i]'
136 94
137 95
138 96 class BoldPattern(TextFormatter):
139 97 name = 'b'
140 98 preview_left = '<b>'
141 99 preview_right = '</b>'
142 100
143 format_left = '__'
144 format_right = '__'
101 format_left = '[b]'
102 format_right = '[/b]'
145 103
146 104
147 105 class CodePattern(TextFormatter):
148 106 name = 'code'
149 107 preview_left = '<code>'
150 108 preview_right = '</code>'
151 109
152 format_left = ' '
153
154
155 class DashPattern(Pattern):
156 def handleMatch(self, m):
157 return u'—'
110 format_left = '[code]'
111 format_right = '[/code]'
158 112
159 113
160 class NeboardMarkdown(markdown.Extension):
161 def extendMarkdown(self, md, md_globals):
162 self._add_neboard_patterns(md)
163 self._delete_patterns(md)
114 def render_reflink(tag_name, value, options, parent, context):
115 if not REFLINK_PATTERN.match(value):
116 return u'>>%s' % value
164 117
165 def _delete_patterns(self, md):
166 del md.parser.blockprocessors['quote']
167
168 del md.inlinePatterns['image_link']
169 del md.inlinePatterns['image_reference']
118 post_id = int(value)
170 119
171 def _add_neboard_patterns(self, md):
172 autolink = AutolinkPattern(AUTOLINK_PATTERN, md)
173 quote = QuotePattern(QUOTE_PATTERN, md)
174 reflink = ReflinkPattern(REFLINK_PATTERN, md)
175 spoiler = SpoilerPattern(SPOILER_PATTERN, md)
176 comment = CommentPattern(COMMENT_PATTERN, md)
177 strikethrough = StrikeThroughPattern(STRIKETHROUGH_PATTERN, md)
178 dash = DashPattern(DASH_PATTERN, md)
120 posts = boards.models.Post.objects.filter(id=post_id)
121 if posts.exists():
122 post = posts[0]
179 123
180 md.inlinePatterns[u'autolink_ext'] = autolink
181 md.inlinePatterns[u'spoiler'] = spoiler
182 md.inlinePatterns[u'strikethrough'] = strikethrough
183 md.inlinePatterns[u'comment'] = comment
184 md.inlinePatterns[u'reflink'] = reflink
185 md.inlinePatterns[u'quote'] = quote
186 md.inlinePatterns[u'dash'] = dash
124 return u'<a href=%s>&gt;&gt;%s</a>' % (post.get_url(), post_id)
125 else:
126 return u'>>%s' % value
187 127
188 128
189 def make_extension(configs=None):
190 return NeboardMarkdown(configs=configs)
191
192 neboard_extension = make_extension()
193
194
195 def markdown_extended(markup):
196 return markdown.markdown(markup, [neboard_extension, 'nl2br'],
197 safe_mode='escape')
129 def bbcode_extended(markup):
130 parser = bbcode.Parser(newline='</p><p>')
131 parser.add_formatter('post', render_reflink, strip=True)
132 parser.add_simple_formatter('quote',
133 u'<span class="multiquote">%(value)s</span>')
134 parser.add_simple_formatter('comment',
135 u'<span class="comment">//%(value)s</span>')
136 parser.add_simple_formatter('spoiler',
137 u'<span class="spoiler">%(value)s</span>')
138 parser.add_simple_formatter('s',
139 u'<span class="strikethrough">%(value)s</span>')
140 parser.add_simple_formatter('code',
141 u'<pre><code>%(value)s</pre></code>')
142 return '<p>%s</p>' % parser.format(markup)
198 143
199 144 formatters = [
200 145 QuotePattern,
201 146 SpoilerPattern,
202 147 ItalicPattern,
203 148 BoldPattern,
204 149 CommentPattern,
205 150 StrikeThroughPattern,
206 151 CodePattern,
207 152 ]
@@ -1,9 +1,7 b''
1 1 __author__ = 'neko259'
2 2
3 3 from boards.models.image import PostImage
4 4 from boards.models.thread import Thread
5 5 from boards.models.post import Post
6 6 from boards.models.tag import Tag
7 7 from boards.models.user import Ban
8 from boards.models.user import Setting
9 from boards.models.user import User
@@ -1,351 +1,343 b''
1 1 from datetime import datetime, timedelta, date
2 2 from datetime import time as dtime
3 3 import logging
4 4 import re
5 5
6 6 from django.core.cache import cache
7 7 from django.core.urlresolvers import reverse
8 8 from django.db import models, transaction
9 9 from django.template.loader import render_to_string
10 10 from django.utils import timezone
11 11 from markupfield.fields import MarkupField
12 12
13 13 from boards.models import PostImage
14 14 from boards.models.base import Viewable
15 15 from boards.models.thread import Thread
16 16
17 17
18 18 APP_LABEL_BOARDS = 'boards'
19 19
20 20 CACHE_KEY_PPD = 'ppd'
21 21 CACHE_KEY_POST_URL = 'post_url'
22 22
23 POSTS_PER_DAY_RANGE = range(7)
23 POSTS_PER_DAY_RANGE = 7
24 24
25 25 BAN_REASON_AUTO = 'Auto'
26 26
27 27 IMAGE_THUMB_SIZE = (200, 150)
28 28
29 29 TITLE_MAX_LENGTH = 200
30 30
31 DEFAULT_MARKUP_TYPE = 'markdown'
31 DEFAULT_MARKUP_TYPE = 'bbcode'
32 32
33 33 # TODO This should be removed
34 34 NO_IP = '0.0.0.0'
35 35
36 36 # TODO Real user agent should be saved instead of this
37 37 UNKNOWN_UA = ''
38 38
39 SETTING_MODERATE = "moderate"
40
41 REGEX_REPLY = re.compile('>>(\d+)')
39 REGEX_REPLY = re.compile(r'&gt;&gt;(\d+)')
42 40
43 41 logger = logging.getLogger(__name__)
44 42
45 43
46 44 class PostManager(models.Manager):
47
48 def create_post(self, title, text, image=None, thread=None,
49 ip=NO_IP, tags=None, user=None):
45 def create_post(self, title, text, image=None, thread=None, ip=NO_IP,
46 tags=None):
50 47 """
51 48 Creates new post
52 49 """
53 50
54 51 posting_time = timezone.now()
55 52 if not thread:
56 53 thread = Thread.objects.create(bump_time=posting_time,
57 54 last_edit_time=posting_time)
58 55 new_thread = True
59 56 else:
60 57 thread.bump()
61 58 thread.last_edit_time = posting_time
62 59 thread.save()
63 60 new_thread = False
64 61
65 62 post = self.create(title=title,
66 63 text=text,
67 64 pub_time=posting_time,
68 65 thread_new=thread,
69 66 poster_ip=ip,
70 67 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
71 68 # last!
72 last_edit_time=posting_time,
73 user=user)
69 last_edit_time=posting_time)
74 70
75 71 if image:
76 72 post_image = PostImage.objects.create(image=image)
77 73 post.images.add(post_image)
78 74 logger.info('Created image #%d for post #%d' % (post_image.id,
79 75 post.id))
80 76
81 77 thread.replies.add(post)
82 78 if tags:
83 linked_tags = []
84 for tag in tags:
85 tag_linked_tags = tag.get_linked_tags()
86 if len(tag_linked_tags) > 0:
87 linked_tags.extend(tag_linked_tags)
88
89 tags.extend(linked_tags)
90 79 map(thread.add_tag, tags)
91 80
92 81 if new_thread:
93 82 Thread.objects.process_oldest_threads()
94 83 self.connect_replies(post)
95 84
96 logger.info('Created post #%d' % post.id)
85 logger.info('Created post #%d with title %s' % (post.id,
86 post.get_title()))
97 87
98 88 return post
99 89
100 90 def delete_post(self, post):
101 91 """
102 92 Deletes post and update or delete its thread
103 93 """
104 94
105 95 post_id = post.id
106 96
107 97 thread = post.get_thread()
108 98
109 99 if post.is_opening():
110 100 thread.delete()
111 101 else:
112 102 thread.last_edit_time = timezone.now()
113 103 thread.save()
114 104
115 105 post.delete()
116 106
117 logger.info('Deleted post #%d' % post_id)
107 logger.info('Deleted post #%d (%s)' % (post_id, post.get_title()))
118 108
119 109 def delete_posts_by_ip(self, ip):
120 110 """
121 111 Deletes all posts of the author with same IP
122 112 """
123 113
124 114 posts = self.filter(poster_ip=ip)
125 115 map(self.delete_post, posts)
126 116
127 117 def connect_replies(self, post):
128 118 """
129 119 Connects replies to a post to show them as a reflink map
130 120 """
131 121
132 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
122 for reply_number in re.finditer(REGEX_REPLY, post.text.rendered):
133 123 post_id = reply_number.group(1)
134 124 ref_post = self.filter(id=post_id)
135 125 if ref_post.count() > 0:
136 126 referenced_post = ref_post[0]
137 127 referenced_post.referenced_posts.add(post)
138 128 referenced_post.last_edit_time = post.pub_time
139 129 referenced_post.build_refmap()
140 130 referenced_post.save(update_fields=['refmap', 'last_edit_time'])
141 131
142 132 referenced_thread = referenced_post.get_thread()
143 133 referenced_thread.last_edit_time = post.pub_time
144 134 referenced_thread.save(update_fields=['last_edit_time'])
145 135
146 136 def get_posts_per_day(self):
147 137 """
148 138 Gets average count of posts per day for the last 7 days
149 139 """
150 140
151 today = date.today()
152 ppd = cache.get(CACHE_KEY_PPD + str(today))
141 day_end = date.today()
142 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
143
144 cache_key = CACHE_KEY_PPD + str(day_end)
145 ppd = cache.get(cache_key)
153 146 if ppd:
154 147 return ppd
155 148
156 posts_per_days = []
157 for i in POSTS_PER_DAY_RANGE:
158 day_end = today - timedelta(i + 1)
159 day_start = today - timedelta(i + 2)
160
161 149 day_time_start = timezone.make_aware(datetime.combine(
162 150 day_start, dtime()), timezone.get_current_timezone())
163 151 day_time_end = timezone.make_aware(datetime.combine(
164 152 day_end, dtime()), timezone.get_current_timezone())
165 153
166 posts_per_days.append(float(self.filter(
154 posts_per_period = float(self.filter(
167 155 pub_time__lte=day_time_end,
168 pub_time__gte=day_time_start).count()))
156 pub_time__gte=day_time_start).count())
169 157
170 ppd = (sum(posts_per_day for posts_per_day in posts_per_days) /
171 len(posts_per_days))
172 cache.set(CACHE_KEY_PPD + str(today), ppd)
158 ppd = posts_per_period / POSTS_PER_DAY_RANGE
159
160 cache.set(cache_key, ppd)
173 161 return ppd
174 162
175 163
176 164 class Post(models.Model, Viewable):
177 165 """A post is a message."""
178 166
179 167 objects = PostManager()
180 168
181 169 class Meta:
182 170 app_label = APP_LABEL_BOARDS
183 171 ordering = ('id',)
184 172
185 173 title = models.CharField(max_length=TITLE_MAX_LENGTH)
186 174 pub_time = models.DateTimeField()
187 175 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
188 176 escape_html=False)
189 177
190 178 images = models.ManyToManyField(PostImage, null=True, blank=True,
191 179 related_name='ip+', db_index=True)
192 180
193 181 poster_ip = models.GenericIPAddressField()
194 182 poster_user_agent = models.TextField()
195 183
196 184 thread_new = models.ForeignKey('Thread', null=True, default=None,
197 185 db_index=True)
198 186 last_edit_time = models.DateTimeField()
199 user = models.ForeignKey('User', null=True, default=None, db_index=True)
200 187
201 188 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
202 189 null=True,
203 190 blank=True, related_name='rfp+',
204 191 db_index=True)
205 192 refmap = models.TextField(null=True, blank=True)
206 193
207 194 def __unicode__(self):
208 195 return '#' + str(self.id) + ' ' + self.title + ' (' + \
209 196 self.text.raw[:50] + ')'
210 197
211 198 def get_title(self):
212 199 """
213 200 Gets original post title or part of its text.
214 201 """
215 202
216 203 title = self.title
217 204 if not title:
218 205 title = self.text.rendered
219 206
220 207 return title
221 208
222 209 def build_refmap(self):
210 """
211 Builds a replies map string from replies list. This is a cache to stop
212 the server from recalculating the map on every post show.
213 """
223 214 map_string = ''
224 215
225 216 first = True
226 217 for refpost in self.referenced_posts.all():
227 218 if not first:
228 219 map_string += ', '
229 map_string += '<a href="%s">&gt;&gt;%s</a>' % (refpost.get_url(), refpost.id)
220 map_string += '<a href="%s">&gt;&gt;%s</a>' % (refpost.get_url(),
221 refpost.id)
230 222 first = False
231 223
232 224 self.refmap = map_string
233 225
234 226 def get_sorted_referenced_posts(self):
235 227 return self.refmap
236 228
237 229 def is_referenced(self):
238 230 return len(self.refmap) > 0
239 231
240 232 def is_opening(self):
241 233 """
242 234 Checks if this is an opening post or just a reply.
243 235 """
244 236
245 237 return self.get_thread().get_opening_post_id() == self.id
246 238
247 239 @transaction.atomic
248 240 def add_tag(self, tag):
249 241 edit_time = timezone.now()
250 242
251 243 thread = self.get_thread()
252 244 thread.add_tag(tag)
253 245 self.last_edit_time = edit_time
254 self.save()
246 self.save(update_fields=['last_edit_time'])
255 247
256 248 thread.last_edit_time = edit_time
257 thread.save()
249 thread.save(update_fields=['last_edit_time'])
258 250
259 251 @transaction.atomic
260 252 def remove_tag(self, tag):
261 253 edit_time = timezone.now()
262 254
263 255 thread = self.get_thread()
264 256 thread.remove_tag(tag)
265 257 self.last_edit_time = edit_time
266 self.save()
258 self.save(update_fields=['last_edit_time'])
267 259
268 260 thread.last_edit_time = edit_time
269 thread.save()
261 thread.save(update_fields=['last_edit_time'])
270 262
271 263 def get_url(self, thread=None):
272 264 """
273 265 Gets full url to the post.
274 266 """
275 267
276 268 cache_key = CACHE_KEY_POST_URL + str(self.id)
277 269 link = cache.get(cache_key)
278 270
279 271 if not link:
280 272 if not thread:
281 273 thread = self.get_thread()
282 274
283 275 opening_id = thread.get_opening_post_id()
284 276
285 277 if self.id != opening_id:
286 278 link = reverse('thread', kwargs={
287 279 'post_id': opening_id}) + '#' + str(self.id)
288 280 else:
289 281 link = reverse('thread', kwargs={'post_id': self.id})
290 282
291 283 cache.set(cache_key, link)
292 284
293 285 return link
294 286
295 287 def get_thread(self):
296 288 """
297 289 Gets post's thread.
298 290 """
299 291
300 292 return self.thread_new
301 293
302 294 def get_referenced_posts(self):
303 295 return self.referenced_posts.only('id', 'thread_new')
304 296
305 297 def get_text(self):
306 298 return self.text
307 299
308 300 def get_view(self, moderator=False, need_open_link=False,
309 301 truncated=False, *args, **kwargs):
310 302 if 'is_opening' in kwargs:
311 303 is_opening = kwargs['is_opening']
312 304 else:
313 305 is_opening = self.is_opening()
314 306
315 307 if 'thread' in kwargs:
316 308 thread = kwargs['thread']
317 309 else:
318 310 thread = self.get_thread()
319 311
320 312 if 'can_bump' in kwargs:
321 313 can_bump = kwargs['can_bump']
322 314 else:
323 315 can_bump = thread.can_bump()
324 316
325 317 if is_opening:
326 318 opening_post_id = self.id
327 319 else:
328 320 opening_post_id = thread.get_opening_post_id()
329 321
330 322 return render_to_string('boards/post.html', {
331 323 'post': self,
332 324 'moderator': moderator,
333 325 'is_opening': is_opening,
334 326 'thread': thread,
335 327 'bumpable': can_bump,
336 328 'need_open_link': need_open_link,
337 329 'truncated': truncated,
338 330 'opening_post_id': opening_post_id,
339 331 })
340 332
341 333 def get_first_image(self):
342 334 return self.images.earliest('id')
343 335
344 336 def delete(self, using=None):
345 337 """
346 Delete all post images and the post itself.
338 Deletes all post images and the post itself.
347 339 """
348 340
349 341 self.images.all().delete()
350 342
351 super(Post, self).delete(using) No newline at end of file
343 super(Post, self).delete(using)
@@ -1,100 +1,78 b''
1 1 from django.template.loader import render_to_string
2 from boards.models import Thread, Post
3 2 from django.db import models
4 3 from django.db.models import Count, Sum
5 4 from django.core.urlresolvers import reverse
5
6 from boards.models import Thread
6 7 from boards.models.base import Viewable
7 8
9
8 10 __author__ = 'neko259'
9 11
10 12
11 13 class TagManager(models.Manager):
12 14
13 15 def get_not_empty_tags(self):
14 16 """
15 17 Gets tags that have non-archived threads.
16 18 """
17 19
18 20 tags = self.annotate(Count('threads')) \
19 21 .filter(threads__count__gt=0).order_by('name')
20 22
21 23 return tags
22 24
23 25
24 26 class Tag(models.Model, Viewable):
25 27 """
26 28 A tag is a text node assigned to the thread. The tag serves as a board
27 29 section. There can be multiple tags for each thread
28 30 """
29 31
30 32 objects = TagManager()
31 33
32 34 class Meta:
33 35 app_label = 'boards'
34 36 ordering = ('name',)
35 37
36 38 name = models.CharField(max_length=100, db_index=True)
37 39 threads = models.ManyToManyField(Thread, null=True,
38 40 blank=True, related_name='tag+')
39 linked = models.ForeignKey('Tag', null=True, blank=True)
40 41
41 42 def __unicode__(self):
42 43 return self.name
43 44
44 45 def is_empty(self):
45 46 """
46 47 Checks if the tag has some threads.
47 48 """
48 49
49 50 return self.get_thread_count() == 0
50 51
51 52 def get_thread_count(self):
52 53 return self.threads.count()
53 54
54 def get_linked_tags(self):
55 """
56 Gets tags linked to the current one.
57 """
58
59 tag_list = []
60 self.get_linked_tags_list(tag_list)
61
62 return tag_list
63
64 def get_linked_tags_list(self, tag_list=[]):
65 """
66 Returns the list of tags linked to current. The list can be got
67 through returned value or tag_list parameter
68 """
69
70 linked_tag = self.linked
71
72 if linked_tag and not (linked_tag in tag_list):
73 tag_list.append(linked_tag)
74
75 linked_tag.get_linked_tags_list(tag_list)
76
77 55 def get_post_count(self, archived=False):
78 56 """
79 57 Gets posts count for the tag's threads.
80 58 """
81 59
82 60 posts_count = 0
83 61
84 62 threads = self.threads.filter(archived=archived)
85 63 if threads.exists():
86 64 posts_count = threads.annotate(posts_count=Count('replies')) \
87 65 .aggregate(posts_sum=Sum('posts_count'))['posts_sum']
88 66
89 67 if not posts_count:
90 68 posts_count = 0
91 69
92 70 return posts_count
93 71
94 72 def get_url(self):
95 73 return reverse('tag', kwargs={'tag_name': self.name})
96 74
97 75 def get_view(self, *args, **kwargs):
98 76 return render_to_string('boards/tag.html', {
99 77 'tag': self,
100 78 })
@@ -1,132 +1,20 b''
1 1 from django.db import models
2 from django.db.models import Count
3 from boards import settings
4 from boards.models import Post
5 from django.core.cache import cache
6 2
7 3 __author__ = 'neko259'
8 4
9 RANK_ADMIN = 0
10 RANK_MODERATOR = 10
11 RANK_USER = 100
12
13 5 BAN_REASON_AUTO = 'Auto'
14 6 BAN_REASON_MAX_LENGTH = 200
15 7
16 VETERAN_POSTS = 1000
17
18
19 class User(models.Model):
20
21 class Meta:
22 app_label = 'boards'
23
24 user_id = models.CharField(max_length=50)
25 rank = models.IntegerField()
26
27 registration_time = models.DateTimeField()
28
29 fav_tags = models.ManyToManyField('Tag', null=True, blank=True)
30 fav_threads = models.ManyToManyField(Post, related_name='+', null=True,
31 blank=True)
32
33 hidden_tags = models.ManyToManyField('Tag', null=True, blank=True,
34 related_name='ht+')
35 hidden_threads = models.ManyToManyField('Post', null=True, blank=True,
36 related_name='hth+')
37
38 def save_setting(self, name, value):
39 setting, created = Setting.objects.get_or_create(name=name, user=self)
40 setting.value = str(value)
41 setting.save()
42
43 return setting
44
45 def get_setting(self, name):
46 if Setting.objects.filter(name=name, user=self).exists():
47 setting = Setting.objects.get(name=name, user=self)
48 setting_value = setting.value
49 else:
50 setting_value = None
51
52 return setting_value
53
54 def is_moderator(self):
55 return RANK_MODERATOR >= self.rank
56
57 def get_sorted_fav_tags(self):
58 cache_key = self._get_tag_cache_key()
59 fav_tags = cache.get(cache_key)
60 if fav_tags:
61 return fav_tags
62
63 tags = self.fav_tags.annotate(Count('threads')) \
64 .filter(threads__count__gt=0).order_by('name')
65
66 if tags:
67 cache.set(cache_key, tags)
68
69 return tags
70
71 def get_post_count(self):
72 return Post.objects.filter(user=self).count()
73
74 def __unicode__(self):
75 return self.user_id + '(' + str(self.rank) + ')'
76
77 def get_last_access_time(self):
78 """
79 Gets user's last post time.
80 """
81
82 posts = Post.objects.filter(user=self)
83 if posts.exists() > 0:
84 return posts.latest('pub_time').pub_time
85
86 def add_tag(self, tag):
87 self.fav_tags.add(tag)
88 cache.delete(self._get_tag_cache_key())
89
90 def remove_tag(self, tag):
91 self.fav_tags.remove(tag)
92 cache.delete(self._get_tag_cache_key())
93
94 def hide_tag(self, tag):
95 self.hidden_tags.add(tag)
96
97 def unhide_tag(self, tag):
98 self.hidden_tags.remove(tag)
99
100 def is_veteran(self):
101 """
102 Returns if a user is old (veteran).
103 """
104
105 return self.get_post_count() >= VETERAN_POSTS
106
107 def _get_tag_cache_key(self):
108 return self.user_id + '_tags'
109
110
111 class Setting(models.Model):
112
113 class Meta:
114 app_label = 'boards'
115
116 name = models.CharField(max_length=50)
117 value = models.CharField(max_length=50)
118 user = models.ForeignKey(User)
119
120 8
121 9 class Ban(models.Model):
122 10
123 11 class Meta:
124 12 app_label = 'boards'
125 13
126 14 ip = models.GenericIPAddressField()
127 15 reason = models.CharField(default=BAN_REASON_AUTO,
128 16 max_length=BAN_REASON_MAX_LENGTH)
129 17 can_read = models.BooleanField(default=True)
130 18
131 19 def __unicode__(self):
132 20 return self.ip
@@ -1,20 +1,23 b''
1 1 VERSION = '1.8.1 Kara'
2 2 SITE_NAME = 'Neboard'
3 3
4 4 CACHE_TIMEOUT = 600 # Timeout for caching, if cache is used
5 5 LOGIN_TIMEOUT = 3600 # Timeout between login tries
6 6 MAX_TEXT_LENGTH = 30000 # Max post length in characters
7 7 MAX_IMAGE_SIZE = 8 * 1024 * 1024 # Max image size
8 8
9 9 # Thread bumplimit
10 10 MAX_POSTS_PER_THREAD = 10
11 11 # Old posts will be archived or deleted if this value is reached
12 12 MAX_THREAD_COUNT = 5
13 13 THREADS_PER_PAGE = 3
14 14 DEFAULT_THEME = 'md'
15 15 LAST_REPLIES_COUNT = 3
16 16
17 17 # Enable archiving threads instead of deletion when the thread limit is reached
18 18 ARCHIVE_THREADS = True
19 19 # Limit posting speed
20 LIMIT_POSTING_SPEED = False No newline at end of file
20 LIMIT_POSTING_SPEED = False
21
22 # This password is used to add admin permissions to the user
23 MASTER_PASSWORD = u'password' No newline at end of file
@@ -1,430 +1,440 b''
1 1 html {
2 2 background: #555;
3 3 color: #ffffff;
4 4 }
5 5
6 6 body {
7 7 margin: 0;
8 8 }
9 9
10 10 #admin_panel {
11 11 background: #FF0000;
12 12 color: #00FF00
13 13 }
14 14
15 15 .input_field_error {
16 16 color: #FF0000;
17 17 }
18 18
19 19 .title {
20 20 font-weight: bold;
21 21 color: #ffcc00;
22 22 }
23 23
24 24 .link, a {
25 25 color: #afdcec;
26 26 }
27 27
28 28 .block {
29 29 display: inline-block;
30 30 vertical-align: top;
31 31 }
32 32
33 33 .tag {
34 34 color: #FFD37D;
35 35 }
36 36
37 37 .post_id {
38 38 color: #fff380;
39 39 }
40 40
41 41 .post, .dead_post, .archive_post, #posts-table {
42 42 background: #333;
43 43 padding: 10px;
44 44 clear: left;
45 45 word-wrap: break-word;
46 46 border-top: 1px solid #777;
47 47 border-bottom: 1px solid #777;
48 48 }
49 49
50 50 .post + .post {
51 51 border-top: none;
52 52 }
53 53
54 54 .dead_post + .dead_post {
55 55 border-top: none;
56 56 }
57 57
58 58 .archive_post + .archive_post {
59 59 border-top: none;
60 60 }
61 61
62 62 .metadata {
63 63 padding-top: 5px;
64 64 margin-top: 10px;
65 65 border-top: solid 1px #666;
66 66 color: #ddd;
67 67 }
68 68
69 69 .navigation_panel, .tag_info {
70 70 background: #444;
71 71 margin-bottom: 5px;
72 72 margin-top: 5px;
73 73 padding: 10px;
74 74 border-bottom: solid 1px #888;
75 75 border-top: solid 1px #888;
76 76 color: #eee;
77 77 }
78 78
79 79 .navigation_panel .link {
80 80 border-right: 1px solid #fff;
81 81 font-weight: bold;
82 82 margin-right: 1ex;
83 83 padding-right: 1ex;
84 84 }
85 85 .navigation_panel .link:last-child {
86 86 border-left: 1px solid #fff;
87 87 border-right: none;
88 88 float: right;
89 89 margin-left: 1ex;
90 90 margin-right: 0;
91 91 padding-left: 1ex;
92 92 padding-right: 0;
93 93 }
94 94
95 95 .navigation_panel::after, .post::after {
96 96 clear: both;
97 97 content: ".";
98 98 display: block;
99 99 height: 0;
100 100 line-height: 0;
101 101 visibility: hidden;
102 102 }
103 103
104 104 p {
105 105 margin-top: .5em;
106 106 margin-bottom: .5em;
107 107 }
108 108
109 109 .post-form-w {
110 110 background: #333344;
111 111 border-top: solid 1px #888;
112 112 border-bottom: solid 1px #888;
113 113 color: #fff;
114 114 padding: 10px;
115 115 margin-bottom: 5px;
116 116 margin-top: 5px;
117 117 }
118 118
119 119 .form-row {
120 120 width: 100%;
121 121 }
122 122
123 123 .form-label {
124 124 padding: .25em 1ex .25em 0;
125 125 vertical-align: top;
126 126 }
127 127
128 128 .form-input {
129 129 padding: .25em 0;
130 130 }
131 131
132 132 .form-errors {
133 133 font-weight: bolder;
134 134 vertical-align: middle;
135 135 }
136 136
137 137 .post-form input:not([name="image"]), .post-form textarea {
138 138 background: #333;
139 139 color: #fff;
140 140 border: solid 1px;
141 141 padding: 0;
142 142 font: medium sans-serif;
143 143 width: 100%;
144 144 }
145 145
146 146 .form-submit {
147 147 display: table;
148 148 margin-bottom: 1ex;
149 149 margin-top: 1ex;
150 150 }
151 151
152 152 .form-title {
153 153 font-weight: bold;
154 154 font-size: 2ex;
155 155 margin-bottom: 0.5ex;
156 156 }
157 157
158 158 .post-form input[type="submit"], input[type="submit"] {
159 159 background: #222;
160 160 border: solid 2px #fff;
161 161 color: #fff;
162 162 padding: 0.5ex;
163 163 }
164 164
165 165 input[type="submit"]:hover {
166 166 background: #060;
167 167 }
168 168
169 169 blockquote {
170 170 border-left: solid 2px;
171 171 padding-left: 5px;
172 172 color: #B1FB17;
173 173 margin: 0;
174 174 }
175 175
176 176 .post > .image {
177 177 float: left;
178 178 margin: 0 1ex .5ex 0;
179 179 min-width: 1px;
180 180 text-align: center;
181 181 display: table-row;
182 182 }
183 183
184 184 .post > .metadata {
185 185 clear: left;
186 186 }
187 187
188 188 .get {
189 189 font-weight: bold;
190 190 color: #d55;
191 191 }
192 192
193 193 * {
194 194 text-decoration: none;
195 195 }
196 196
197 197 .dead_post {
198 198 background-color: #442222;
199 199 }
200 200
201 201 .archive_post {
202 202 background-color: #000;
203 203 }
204 204
205 205 .mark_btn {
206 206 border: 1px solid;
207 207 min-width: 2ex;
208 208 padding: 2px 2ex;
209 209 }
210 210
211 211 .mark_btn:hover {
212 212 background: #555;
213 213 }
214 214
215 215 .quote {
216 216 color: #92cf38;
217 217 font-style: italic;
218 218 }
219 219
220 .multiquote {
221 border-left: solid 4px #ccc;
222 padding: 3px;
223 display: inline-block;
224 background: #222;
225 border-right: solid 1px #ccc;
226 border-top: solid 1px #ccc;
227 border-bottom: solid 1px #ccc;
228 }
229
220 230 .spoiler {
221 231 background: white;
222 232 color: white;
223 233 }
224 234
225 235 .spoiler:hover {
226 236 color: black;
227 237 }
228 238
229 239 .comment {
230 240 color: #eb2;
231 241 }
232 242
233 243 a:hover {
234 244 text-decoration: underline;
235 245 }
236 246
237 247 .last-replies {
238 248 margin-left: 3ex;
239 249 margin-right: 3ex;
240 250 }
241 251
242 252 .thread {
243 253 margin-bottom: 3ex;
244 254 margin-top: 1ex;
245 255 }
246 256
247 257 .post:target {
248 258 border: solid 2px white;
249 259 }
250 260
251 261 pre{
252 262 white-space:pre-wrap
253 263 }
254 264
255 265 li {
256 266 list-style-position: inside;
257 267 }
258 268
259 269 .fancybox-skin {
260 270 position: relative;
261 271 background-color: #fff;
262 272 color: #ddd;
263 273 text-shadow: none;
264 274 }
265 275
266 276 .fancybox-image {
267 277 border: 1px solid black;
268 278 }
269 279
270 280 .image-mode-tab {
271 281 background: #444;
272 282 color: #eee;
273 283 margin-top: 5px;
274 284 padding: 5px;
275 285 border-top: 1px solid #888;
276 286 border-bottom: 1px solid #888;
277 287 }
278 288
279 289 .image-mode-tab > label {
280 290 margin: 0 1ex;
281 291 }
282 292
283 293 .image-mode-tab > label > input {
284 294 margin-right: .5ex;
285 295 }
286 296
287 297 #posts-table {
288 298 margin-top: 5px;
289 299 margin-bottom: 5px;
290 300 }
291 301
292 302 .tag_info > h2 {
293 303 margin: 0;
294 304 }
295 305
296 306 .post-info {
297 307 color: #ddd;
298 308 margin-bottom: 1ex;
299 309 }
300 310
301 311 .moderator_info {
302 312 color: #e99d41;
303 313 float: right;
304 314 font-weight: bold;
305 315 }
306 316
307 317 .refmap {
308 318 font-size: 0.9em;
309 319 color: #ccc;
310 320 margin-top: 1em;
311 321 }
312 322
313 323 .fav {
314 324 color: yellow;
315 325 }
316 326
317 327 .not_fav {
318 328 color: #ccc;
319 329 }
320 330
321 331 .role {
322 332 text-decoration: underline;
323 333 }
324 334
325 335 .form-email {
326 336 display: none;
327 337 }
328 338
329 339 .footer {
330 340 margin: 5px;
331 341 }
332 342
333 343 .bar-value {
334 344 background: rgba(50, 55, 164, 0.45);
335 345 font-size: 0.9em;
336 346 height: 1.5em;
337 347 }
338 348
339 349 .bar-bg {
340 350 position: relative;
341 351 border-top: solid 1px #888;
342 352 border-bottom: solid 1px #888;
343 353 margin-top: 5px;
344 354 overflow: hidden;
345 355 }
346 356
347 357 .bar-text {
348 358 padding: 2px;
349 359 position: absolute;
350 360 left: 0;
351 361 top: 0;
352 362 }
353 363
354 364 .page_link {
355 365 background: #444;
356 366 border-top: solid 1px #888;
357 367 border-bottom: solid 1px #888;
358 368 padding: 5px;
359 369 color: #eee;
360 370 font-size: 2ex;
361 371 }
362 372
363 373 .skipped_replies {
364 374 margin: 5px;
365 375 }
366 376
367 377 .current_page {
368 378 border: solid 1px #afdcec;
369 379 padding: 2px;
370 380 }
371 381
372 382 .current_mode {
373 383 font-weight: bold;
374 384 }
375 385
376 386 .gallery_image {
377 387 border: solid 1px;
378 388 padding: 0.5ex;
379 389 margin: 0.5ex;
380 390 text-align: center;
381 391 }
382 392
383 393 code {
384 394 border: dashed 1px #ccc;
385 395 background: #111;
386 396 padding: 2px;
387 397 font-size: 1.2em;
388 398 display: inline-block;
389 399 }
390 400
391 401 pre {
392 402 overflow: auto;
393 403 }
394 404
395 405 .img-full {
396 406 background: #222;
397 407 border: solid 1px white;
398 408 }
399 409
400 410 .tag_item {
401 411 display: inline-block;
402 412 border: 1px dashed #666;
403 413 margin: 0.2ex;
404 414 padding: 0.1ex;
405 415 }
406 416
407 417 #id_models li {
408 418 list-style: none;
409 419 }
410 420
411 421 #id_q {
412 422 margin-left: 1ex;
413 423 }
414 424
415 425 ul {
416 426 padding-left: 0px;
417 427 }
418 428
419 429 /* Reflink preview */
420 430 .post_preview {
421 431 border-left: 1px solid #777;
422 432 border-right: 1px solid #777;
423 433 }
424 434
425 435 /* Code highlighter */
426 436 .hljs {
427 437 color: #fff;
428 438 background: #000;
429 439 display: inline-block;
430 440 }
@@ -1,61 +1,61 b''
1 1 /*
2 2 @licstart The following is the entire license notice for the
3 3 JavaScript code in this page.
4 4
5 5
6 6 Copyright (C) 2013 neko259
7 7
8 8 The JavaScript code in this page is free software: you can
9 9 redistribute it and/or modify it under the terms of the GNU
10 10 General Public License (GNU GPL) as published by the Free Software
11 11 Foundation, either version 3 of the License, or (at your option)
12 12 any later version. The code is distributed WITHOUT ANY WARRANTY;
13 13 without even the implied warranty of MERCHANTABILITY or FITNESS
14 14 FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
15 15
16 16 As additional permission under GNU GPL version 3 section 7, you
17 17 may distribute non-source (e.g., minimized or compacted) forms of
18 18 that code without the copy of the GNU GPL normally required by
19 19 section 4, provided you include this license notice and a URL
20 20 through which recipients can access the Corresponding Source.
21 21
22 22 @licend The above is the entire license notice
23 23 for the JavaScript code in this page.
24 24 */
25 25
26 26 function moveCaretToEnd(el) {
27 27 if (typeof el.selectionStart == "number") {
28 28 el.selectionStart = el.selectionEnd = el.value.length;
29 29 } else if (typeof el.createTextRange != "undefined") {
30 30 el.focus();
31 31 var range = el.createTextRange();
32 32 range.collapse(false);
33 33 range.select();
34 34 }
35 35 }
36 36
37 37 function addQuickReply(postId) {
38 var textToAdd = '>>' + postId + '\n\n';
38 var textToAdd = '[post]' + postId + '[/post]\n';
39 39 var selection = window.getSelection().toString();
40 40 if (selection.length > 0) {
41 textToAdd += '> ' + selection + '\n\n';
41 textToAdd += '[quote]' + selection + '[/quote]\n';
42 42 }
43 43
44 44 var textAreaId = 'textarea';
45 45 $(textAreaId).val($(textAreaId).val()+ textToAdd);
46 46
47 47 var textarea = document.getElementsByTagName('textarea')[0];
48 48 $(textAreaId).focus();
49 49 moveCaretToEnd(textarea);
50 50
51 51 $("html, body").animate({ scrollTop: $(textAreaId).offset().top }, "slow");
52 52 }
53 53
54 54 function scrollToBottom() {
55 55 var $target = $('html,body');
56 56 $target.animate({scrollTop: $target.height()}, "fast");
57 57 }
58 58
59 59 $(document).ready(function() {
60 60 swapForm();
61 61 })
@@ -1,62 +1,66 b''
1 1 {% load staticfiles %}
2 2 {% load i18n %}
3 3 {% load l10n %}
4 4 {% load static from staticfiles %}
5 5
6 6 <!DOCTYPE html>
7 7 <html>
8 8 <head>
9 9 <link rel="stylesheet" type="text/css" href="{% static 'css/base.css' %}" media="all"/>
10 10 <link rel="stylesheet" type="text/css" href="{% static 'css/3party/highlight.css' %}" media="all"/>
11 11 <link rel="stylesheet" type="text/css" href="{% static theme_css %}" media="all"/>
12 12 <link rel="alternate" type="application/rss+xml" href="rss/" title="{% trans 'Feed' %}"/>
13 13
14 14 <link rel="icon" type="image/png"
15 15 href="{% static 'favicon.png' %}">
16 16
17 17 <meta name="viewport" content="width=device-width, initial-scale=1"/>
18 18 <meta charset="utf-8"/>
19 19
20 20 {% block head %}{% endblock %}
21 21 </head>
22 22 <body>
23 23 <script src="{% static 'js/jquery-2.0.1.min.js' %}"></script>
24 24 <script src="{% static 'js/jquery-ui-1.10.3.custom.min.js' %}"></script>
25 25 <script src="{% static 'js/jquery.mousewheel.js' %}"></script>
26 26 <script src="{% url 'js_info_dict' %}"></script>
27 27
28 28 <div class="navigation_panel">
29 29 <a class="link" href="{% url 'index' %}">{% trans "All threads" %}</a>
30 30 {% for tag in tags %}
31 31 <a class="tag" href="{% url 'tag' tag_name=tag.name %}"
32 32 >#{{ tag.name }}</a>,
33 33 {% endfor %}
34 34 <a href="{% url 'tags' %}" title="{% trans 'Tag management' %}"
35 35 >[...]</a>
36 36 <a class="link" href="{% url 'settings' %}">{% trans 'Settings' %}</a>
37 37 </div>
38 38
39 39 {% block content %}{% endblock %}
40 40
41 41 <script src="{% static 'js/popup.js' %}"></script>
42 42 <script src="{% static 'js/image.js' %}"></script>
43 43 <script src="{% static 'js/3party/highlight.min.js' %}"></script>
44 44 <script src="{% static 'js/refpopup.js' %}"></script>
45 45 <script src="{% static 'js/main.js' %}"></script>
46 46
47 47 <div class="navigation_panel">
48 48 {% block metapanel %}{% endblock %}
49 {% if moderator %}
50 [<a href="{% url "logout" %}">{% trans 'Logout' %}</a>]
51 {% else %}
49 52 [<a href="{% url "login" %}">{% trans 'Login' %}</a>]
53 {% endif %}
50 54 [<a href="{% url "search" %}">{% trans 'Search' %}</a>]
51 55 {% with ppd=posts_per_day|floatformat:2 %}
52 56 {% blocktrans %}Speed: {{ ppd }} posts per day{% endblocktrans %}
53 57 {% endwith %}
54 58 <a class="link" href="#top">{% trans 'Up' %}</a>
55 59 </div>
56 60
57 61 <div class="footer">
58 62 <!-- Put your banners here -->
59 63 </div>
60 64
61 65 </body>
62 66 </html>
@@ -1,99 +1,99 b''
1 1 {% load i18n %}
2 2 {% load board %}
3 3 {% load cache %}
4 4
5 5 {% get_current_language as LANGUAGE_CODE %}
6 6
7 7 {% spaceless %}
8 8 {% cache 600 post post.id post.last_edit_time thread.archived bumpable truncated moderator LANGUAGE_CODE need_open_link %}
9 9 {% if thread.archived %}
10 10 <div class="post archive_post" id="{{ post.id }}">
11 11 {% elif bumpable %}
12 12 <div class="post" id="{{ post.id }}">
13 13 {% else %}
14 14 <div class="post dead_post" id="{{ post.id }}">
15 15 {% endif %}
16 16
17 17 <div class="post-info">
18 18 <a class="post_id" href="{% post_object_url post thread=thread %}"
19 19 {% if not truncated and not thread.archived %}
20 20 onclick="javascript:addQuickReply('{{ post.id }}'); return false;"
21 21 title="{% trans 'Quote' %}"
22 22 {% endif %}
23 23 >({{ post.id }}) </a>
24 24 <span class="title">{{ post.title }} </span>
25 25 <span class="pub_time">{{ post.pub_time }}</span>
26 26 {% if thread.archived %}
27 27 — {{ thread.bump_time }}
28 28 {% endif %}
29 29 {% if is_opening and need_open_link %}
30 30 {% if thread.archived %}
31 31 [<a class="link" href="{% url 'thread' post.id %}">{% trans "Open" %}</a>]
32 32 {% else %}
33 33 [<a class="link" href="{% url 'thread' post.id %}#form">{% trans "Reply" %}</a>]
34 34 {% endif %}
35 35 {% endif %}
36 36
37 37 {% if moderator %}
38 38 <span class="moderator_info">
39 39 [<a href="{% url 'post_admin' post_id=post.id %}"
40 40 >{% trans 'Edit' %}</a>]
41 41 [<a href="{% url 'delete' post_id=post.id %}"
42 42 >{% trans 'Delete' %}</a>]
43 43 ({{ post.poster_ip }})
44 44 [<a href="{% url 'ban' post_id=post.id %}?next={{ request.path }}"
45 45 >{% trans 'Ban IP' %}</a>]
46 46 </span>
47 47 {% endif %}
48 48 </div>
49 49 {% if post.images.exists %}
50 50 {% with post.images.all.0 as image %}
51 51 <div class="image">
52 52 <a
53 53 class="thumb"
54 54 href="{{ image.image.url }}"><img
55 55 src="{{ image.image.url_200x150 }}"
56 56 alt="{{ post.id }}"
57 57 width="{{ image.pre_width }}"
58 58 height="{{ image.pre_height }}"
59 59 data-width="{{ image.width }}"
60 60 data-height="{{ image.height }}"/>
61 61 </a>
62 62 </div>
63 63 {% endwith %}
64 64 {% endif %}
65 65 <div class="message">
66 66 {% autoescape off %}
67 67 {% if truncated %}
68 68 {{ post.text.rendered|truncatewords_html:50 }}
69 69 {% else %}
70 70 {{ post.text.rendered }}
71 71 {% endif %}
72 72 {% endautoescape %}
73 73 {% if post.is_referenced %}
74 74 <div class="refmap">
75 75 {% autoescape off %}
76 76 {% trans "Replies" %}: {{ post.refmap }}
77 77 {% endautoescape %}
78 78 </div>
79 79 {% endif %}
80 80 </div>
81 81 {% endcache %}
82 82 {% if is_opening %}
83 83 {% cache 600 post_thread thread.id thread.last_edit_time LANGUAGE_CODE need_open_link %}
84 84 <div class="metadata">
85 85 {% if is_opening and need_open_link %}
86 {{ thread.get_reply_count }} {% trans 'replies' %},
86 {{ thread.get_reply_count }} {% trans 'messages' %},
87 87 {{ thread.get_images_count }} {% trans 'images' %}.
88 88 {% endif %}
89 89 <span class="tags">
90 90 {% for tag in thread.get_tags %}
91 91 <a class="tag" href="{% url 'tag' tag.name %}">
92 92 #{{ tag.name }}</a>{% if not forloop.last %},{% endif %}
93 93 {% endfor %}
94 94 </span>
95 95 </div>
96 96 {% endcache %}
97 97 {% endif %}
98 98 </div>
99 99 {% endspaceless %}
@@ -1,197 +1,197 b''
1 1 {% extends "boards/base.html" %}
2 2
3 3 {% load i18n %}
4 4 {% load cache %}
5 5 {% load board %}
6 6 {% load static %}
7 7
8 8 {% block head %}
9 9 {% if tag %}
10 10 <title>{{ tag.name }} - {{ site_name }}</title>
11 11 {% else %}
12 12 <title>{{ site_name }}</title>
13 13 {% endif %}
14 14
15 15 {% if current_page.has_previous %}
16 16 <link rel="prev" href="
17 17 {% if tag %}
18 18 {% url "tag" tag_name=tag page=current_page.previous_page_number %}
19 19 {% elif archived %}
20 20 {% url "archive" page=current_page.previous_page_number %}
21 21 {% else %}
22 22 {% url "index" page=current_page.previous_page_number %}
23 23 {% endif %}
24 24 " />
25 25 {% endif %}
26 26 {% if current_page.has_next %}
27 27 <link rel="next" href="
28 28 {% if tag %}
29 29 {% url "tag" tag_name=tag page=current_page.next_page_number %}
30 30 {% elif archived %}
31 31 {% url "archive" page=current_page.next_page_number %}
32 32 {% else %}
33 33 {% url "index" page=current_page.next_page_number %}
34 34 {% endif %}
35 35 " />
36 36 {% endif %}
37 37
38 38 {% endblock %}
39 39
40 40 {% block content %}
41 41
42 42 {% get_current_language as LANGUAGE_CODE %}
43 43
44 44 {% if tag %}
45 45 <div class="tag_info">
46 46 <h2>
47 {% if tag in user.fav_tags.all %}
47 {% if tag in fav_tags %}
48 48 <a href="{% url 'tag' tag.name %}?method=unsubscribe&next={{ request.path }}"
49 49 class="fav"></a>
50 50 {% else %}
51 51 <a href="{% url 'tag' tag.name %}?method=subscribe&next={{ request.path }}"
52 52 class="not_fav"></a>
53 53 {% endif %}
54 {% if tag in user.hidden_tags.all %}
54 {% if tag in hidden_tags %}
55 55 <a href="{% url 'tag' tag.name %}?method=unhide&next={{ request.path }}"
56 56 title="{% trans 'Show tag' %}"
57 57 class="fav">H</a>
58 58 {% else %}
59 59 <a href="{% url 'tag' tag.name %}?method=hide&next={{ request.path }}"
60 60 title="{% trans 'Hide tag' %}"
61 61 class="not_fav">H</a>
62 62 {% endif %}
63 63 #{{ tag.name }}
64 64 </h2>
65 65 </div>
66 66 {% endif %}
67 67
68 68 {% if threads %}
69 69 {% if current_page.has_previous %}
70 70 <div class="page_link">
71 71 <a href="
72 72 {% if tag %}
73 73 {% url "tag" tag_name=tag page=current_page.previous_page_number %}
74 74 {% elif archived %}
75 75 {% url "archive" page=current_page.previous_page_number %}
76 76 {% else %}
77 77 {% url "index" page=current_page.previous_page_number %}
78 78 {% endif %}
79 79 ">{% trans "Previous page" %}</a>
80 80 </div>
81 81 {% endif %}
82 82
83 83 {% for thread in threads %}
84 84 {% cache 600 thread_short thread.id thread.last_edit_time moderator LANGUAGE_CODE %}
85 85 <div class="thread">
86 86 {% with can_bump=thread.can_bump %}
87 87 {% post_view thread.get_opening_post moderator is_opening=True thread=thread can_bump=can_bump truncated=True need_open_link=True %}
88 88 {% if not thread.archived %}
89 89 {% with last_replies=thread.get_last_replies %}
90 90 {% if last_replies %}
91 91 {% if thread.get_skipped_replies_count %}
92 92 <div class="skipped_replies">
93 93 <a href="{% url 'thread' thread.get_opening_post.id %}">
94 94 {% blocktrans with count=thread.get_skipped_replies_count %}Skipped {{ count }} replies. Open thread to see all replies.{% endblocktrans %}
95 95 </a>
96 96 </div>
97 97 {% endif %}
98 98 <div class="last-replies">
99 99 {% for post in last_replies %}
100 100 {% post_view post moderator=moderator is_opening=False thread=thread can_bump=can_bump truncated=True %}
101 101 {% endfor %}
102 102 </div>
103 103 {% endif %}
104 104 {% endwith %}
105 105 {% endif %}
106 106 {% endwith %}
107 107 </div>
108 108 {% endcache %}
109 109 {% endfor %}
110 110
111 111 {% if current_page.has_next %}
112 112 <div class="page_link">
113 113 <a href="
114 114 {% if tag %}
115 115 {% url "tag" tag_name=tag page=current_page.next_page_number %}
116 116 {% elif archived %}
117 117 {% url "archive" page=current_page.next_page_number %}
118 118 {% else %}
119 119 {% url "index" page=current_page.next_page_number %}
120 120 {% endif %}
121 121 ">{% trans "Next page" %}</a>
122 122 </div>
123 123 {% endif %}
124 124 {% else %}
125 125 <div class="post">
126 126 {% trans 'No threads exist. Create the first one!' %}</div>
127 127 {% endif %}
128 128
129 129 <div class="post-form-w">
130 130 <script src="{% static 'js/panel.js' %}"></script>
131 131 <div class="post-form">
132 132 <div class="form-title">{% trans "Create new thread" %}</div>
133 133 <div class="swappable-form-full">
134 134 <form enctype="multipart/form-data" method="post">{% csrf_token %}
135 135 {{ form.as_div }}
136 136 <div class="form-submit">
137 137 <input type="submit" value="{% trans "Post" %}"/>
138 138 </div>
139 139 </form>
140 140 </div>
141 141 <div>
142 142 {% trans 'Tags must be delimited by spaces. Text or image is required.' %}
143 143 </div>
144 144 <div><a href="{% url "staticpage" name="help" %}">
145 145 {% trans 'Text syntax' %}</a></div>
146 146 </div>
147 147 </div>
148 148
149 149 <script src="{% static 'js/form.js' %}"></script>
150 150
151 151 {% endblock %}
152 152
153 153 {% block metapanel %}
154 154
155 155 <span class="metapanel">
156 156 <b><a href="{% url "authors" %}">{{ site_name }}</a> {{ version }}</b>
157 157 {% trans "Pages:" %}
158 158 <a href="
159 159 {% if tag %}
160 160 {% url "tag" tag_name=tag page=paginator.page_range|first %}
161 161 {% elif archived %}
162 162 {% url "archive" page=paginator.page_range|first %}
163 163 {% else %}
164 164 {% url "index" page=paginator.page_range|first %}
165 165 {% endif %}
166 166 ">&lt;&lt;</a>
167 167 [
168 168 {% for page in paginator.center_range %}
169 169 <a
170 170 {% ifequal page current_page.number %}
171 171 class="current_page"
172 172 {% endifequal %}
173 173 href="
174 174 {% if tag %}
175 175 {% url "tag" tag_name=tag page=page %}
176 176 {% elif archived %}
177 177 {% url "archive" page=page %}
178 178 {% else %}
179 179 {% url "index" page=page %}
180 180 {% endif %}
181 181 ">{{ page }}</a>
182 182 {% if not forloop.last %},{% endif %}
183 183 {% endfor %}
184 184 ]
185 185 <a href="
186 186 {% if tag %}
187 187 {% url "tag" tag_name=tag page=paginator.page_range|last %}
188 188 {% elif archived %}
189 189 {% url "archive" page=paginator.page_range|last %}
190 190 {% else %}
191 191 {% url "index" page=paginator.page_range|last %}
192 192 {% endif %}
193 193 ">&gt;&gt;</a>
194 194 [<a href="rss/">RSS</a>]
195 195 </span>
196 196
197 197 {% endblock %}
@@ -1,50 +1,41 b''
1 1 {% extends "boards/base.html" %}
2 2
3 3 {% load i18n %}
4 4 {% load humanize %}
5 5
6 6 {% block head %}
7 7 <title>{% trans 'Settings' %} - {{ site_name }}</title>
8 8 {% endblock %}
9 9
10 10 {% block content %}
11 11
12 12 <div class="post">
13 13 <p>
14 {% trans 'User:' %} <b>{{ user.user_id }}</b>.
15 {% if user.is_moderator %}
14 {% if moderator %}
16 15 {% trans 'You are moderator.' %}
17 16 {% endif %}
18 {% if user.is_veteran %}
19 {% trans 'You are veteran.' %}
20 {% endif %}
21 17 </p>
22 <p>{% trans 'Posts:' %} {{ user.get_post_count }}</p>
23 <p>{% trans 'First access:' %} {{ user.registration_time|naturaltime }}</p>
24 {% if user.get_last_access_time %}
25 <p>{% trans 'Last access:' %} {{ user.get_last_access_time|naturaltime }}</p>
26 {% endif %}
27 {% with hidden_tags=user.hidden_tags.all %}
28 18 {% if hidden_tags %}
29 19 <p>{% trans 'Hidden tags:' %}
30 20 {% for tag in hidden_tags %}
31 21 <a class="tag" href="{% url 'tag' tag.name %}">
32 22 #{{ tag.name }}</a>{% if not forloop.last %},{% endif %}
33 23 {% endfor %}
34 24 </p>
25 {% else %}
26 <p>{% trans 'No hidden tags.' %}</p>
35 27 {% endif %}
36 {% endwith %}
37 28 </div>
38 29
39 30 <div class="post-form-w">
40 31 <div class="post-form">
41 32 <form method="post">{% csrf_token %}
42 33 {{ form.as_div }}
43 34 <div class="form-submit">
44 35 <input type="submit" value="{% trans "Save" %}" />
45 36 </div>
46 37 </form>
47 38 </div>
48 39 </div>
49 40
50 41 {% endblock %}
@@ -1,20 +1,18 b''
1 1 {% extends "boards/static_base.html" %}
2 2
3 3 {% load i18n %}
4 4
5 5 {% block head %}
6 6 <title>{% trans "Syntax" %}</title>
7 7 {% endblock %}
8 8
9 9 {% block staticcontent %}
10 10 <h2>{% trans 'Syntax' %}</h2>
11 <p>{% trans '2 line breaks for a new line.' %}</p>
12 <p>_<i>{% trans 'Italic text' %}</i>_</p>
13 <p>__<b>{% trans 'Bold text' %}</b>__</p>
14 <p>%%<span class="spoiler">{% trans 'Spoiler' %}</span>%%</p>
15 <p><a>>>123</a> -- {% trans 'Link to a post' %}</p>
16 <p>~<span class="strikethrough">{% trans 'Strikethrough text' %}</span>~</p>
17 <p>{% trans 'You need to new line before:' %}</p>
18 <p><span class="comment">//{% trans 'Comment' %}</span></p>
19 <p><span class="quote">> {% trans 'Quote' %}</span></p>
11 <p>[i]<i>{% trans 'Italic text' %}</i>[/i]</p>
12 <p>[b]<b>{% trans 'Bold text' %}</b>[/b]</p>
13 <p>[spoiler]<span class="spoiler">{% trans 'Spoiler' %}</span>[/spoiler]</p>
14 <p>[post]123[/post] -- {% trans 'Link to a post' %}</p>
15 <p>[s]<span class="strikethrough">{% trans 'Strikethrough text' %}</span>[/s]</p>
16 <p>[comment]<span class="comment">{% trans 'Comment' %}</span>[/comment]</p>
17 <p>[quote]<span class="multiquote">{% trans 'Quote' %}</span>[/quote]</p>
20 18 {% endblock %}
@@ -1,95 +1,95 b''
1 1 {% extends "boards/base.html" %}
2 2
3 3 {% load i18n %}
4 4 {% load cache %}
5 5 {% load static from staticfiles %}
6 6 {% load board %}
7 7
8 8 {% block head %}
9 9 <title>{{ opening_post.get_title|striptags|truncatewords:10 }}
10 10 - {{ site_name }}</title>
11 11 {% endblock %}
12 12
13 13 {% block content %}
14 14 {% spaceless %}
15 15 {% get_current_language as LANGUAGE_CODE %}
16 16
17 17 {% cache 600 thread_view thread.id thread.last_edit_time moderator LANGUAGE_CODE %}
18 18
19 19 <div class="image-mode-tab">
20 20 <a class="current_mode" href="{% url 'thread' opening_post.id %}">{% trans 'Normal mode' %}</a>,
21 21 <a href="{% url 'thread_mode' opening_post.id 'gallery' %}">{% trans 'Gallery mode' %}</a>
22 22 </div>
23 23
24 24 {% if bumpable %}
25 25 <div class="bar-bg">
26 26 <div class="bar-value" style="width:{{ bumplimit_progress }}%" id="bumplimit_progress">
27 27 </div>
28 28 <div class="bar-text">
29 29 <span id="left_to_limit">{{ posts_left }}</span> {% trans 'posts to bumplimit' %}
30 30 </div>
31 31 </div>
32 32 {% endif %}
33 33
34 34 <div class="thread">
35 35 {% with can_bump=thread.can_bump %}
36 36 {% for post in thread.get_replies %}
37 37 {% if forloop.first %}
38 38 {% post_view post moderator=moderator is_opening=True thread=thread can_bump=can_bump opening_post_id=opening_post.id %}
39 39 {% else %}
40 40 {% post_view post moderator=moderator is_opening=False thread=thread can_bump=can_bump opening_post_id=opening_post.id %}
41 41 {% endif %}
42 42 {% endfor %}
43 43 {% endwith %}
44 44 </div>
45 45
46 46 {% if not thread.archived %}
47 47
48 48 <div class="post-form-w" id="form">
49 49 <script src="{% static 'js/panel.js' %}"></script>
50 50 <div class="form-title">{% trans "Reply to thread" %} #{{ opening_post.id }}</div>
51 51 <div class="post-form" id="compact-form">
52 52 <div class="swappable-form-full">
53 53 <form enctype="multipart/form-data" method="post"
54 54 >{% csrf_token %}
55 55 <div class="compact-form-text"></div>
56 56 {{ form.as_div }}
57 57 <div class="form-submit">
58 58 <input type="submit" value="{% trans "Post" %}"/>
59 59 </div>
60 60 </form>
61 61 </div>
62 62 <a onclick="swapForm(); return false;" href="#">
63 63 {% trans 'Switch mode' %}
64 64 </a>
65 65 <div><a href="{% url "staticpage" name="help" %}">
66 66 {% trans 'Text syntax' %}</a></div>
67 67 </div>
68 68 </div>
69 69
70 70 <script src="{% static 'js/jquery.form.min.js' %}"></script>
71 71 <script src="{% static 'js/thread_update.js' %}"></script>
72 72 {% endif %}
73 73
74 74 <script src="{% static 'js/form.js' %}"></script>
75 75 <script src="{% static 'js/thread.js' %}"></script>
76 76
77 77 {% endcache %}
78 78
79 79 {% endspaceless %}
80 80 {% endblock %}
81 81
82 82 {% block metapanel %}
83 83
84 84 {% get_current_language as LANGUAGE_CODE %}
85 85
86 86 <span class="metapanel" data-last-update="{{ last_update }}">
87 87 {% cache 600 thread_meta thread.last_edit_time moderator LANGUAGE_CODE %}
88 <span id="reply-count">{{ thread.get_reply_count }}</span>/{{ max_replies }} {% trans 'replies' %},
88 <span id="reply-count">{{ thread.get_reply_count }}</span>/{{ max_replies }} {% trans 'messages' %},
89 89 <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}.
90 90 {% trans 'Last update: ' %}<span id="last-update">{{ thread.last_edit_time }}</span>
91 91 [<a href="rss/">RSS</a>]
92 92 {% endcache %}
93 93 </span>
94 94
95 95 {% endblock %}
@@ -1,68 +1,68 b''
1 1 {% extends "boards/base.html" %}
2 2
3 3 {% load i18n %}
4 4 {% load cache %}
5 5 {% load static from staticfiles %}
6 6 {% load board %}
7 7
8 8 {% block head %}
9 9 <title>{{ thread.get_opening_post.get_title|striptags|truncatewords:10 }}
10 10 - {{ site_name }}</title>
11 11 {% endblock %}
12 12
13 13 {% block content %}
14 14 {% spaceless %}
15 15 {% get_current_language as LANGUAGE_CODE %}
16 16
17 17 <script src="{% static 'js/thread.js' %}"></script>
18 18
19 19 {% cache 600 thread_gallery_view thread.id thread.last_edit_time LANGUAGE_CODE request.get_host %}
20 20 <div class="image-mode-tab">
21 21 <a href="{% url 'thread' thread.get_opening_post.id %}">{% trans 'Normal mode' %}</a>,
22 22 <a class="current_mode" href="{% url 'thread_mode' thread.get_opening_post.id 'gallery' %}">{% trans 'Gallery mode' %}</a>
23 23 </div>
24 24
25 25 <div id="posts-table">
26 26 {% for post in posts %}
27 27 <div class="gallery_image">
28 28 {% with post.get_first_image as image %}
29 29 <div>
30 30 <a
31 31 class="thumb"
32 32 href="{{ image.image.url }}"><img
33 33 src="{{ image.image.url_200x150 }}"
34 34 alt="{{ post.id }}"
35 35 width="{{ image.pre_width }}"
36 36 height="{{ image.pre_height }}"
37 37 data-width="{{ image.width }}"
38 38 data-height="{{ image.height }}"/>
39 39 </a>
40 40 </div>
41 41 <div class="gallery_image_metadata">
42 42 {{ image.width }}x{{ image.height }}
43 43 {% image_actions image.image.url request.get_host %}
44 44 </div>
45 45 {% endwith %}
46 46 </div>
47 47 {% endfor %}
48 48 </div>
49 49 {% endcache %}
50 50
51 51 {% endspaceless %}
52 52 {% endblock %}
53 53
54 54 {% block metapanel %}
55 55
56 56 {% get_current_language as LANGUAGE_CODE %}
57 57
58 58 <span class="metapanel" data-last-update="{{ last_update }}">
59 59 {% cache 600 thread_meta thread.last_edit_time moderator LANGUAGE_CODE %}
60 60 <span id="reply-count">{{ thread.get_reply_count }}</span>/{{ max_replies }}
61 {% trans 'replies' %},
61 {% trans 'messages' %},
62 62 <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}.
63 63 {% trans 'Last update: ' %}{{ thread.last_edit_time }}
64 64 [<a href="rss/">RSS</a>]
65 65 {% endcache %}
66 66 </span>
67 67
68 68 {% endblock %}
@@ -1,260 +1,270 b''
1 1 # coding=utf-8
2 2 import time
3 3 import logging
4 4 from django.core.paginator import Paginator
5 5
6 6 from django.test import TestCase
7 7 from django.test.client import Client
8 8 from django.core.urlresolvers import reverse, NoReverseMatch
9 from boards.abstracts.settingsmanager import get_settings_manager
9 10
10 11 from boards.models import Post, Tag, Thread
11 12 from boards import urls
12 13 from boards import settings
13 14 import neboard
14 15
16 TEST_TAG = 'test_tag'
17
15 18 PAGE_404 = 'boards/404.html'
16 19
17 20 TEST_TEXT = 'test text'
18 21
19 22 NEW_THREAD_PAGE = '/'
20 23 THREAD_PAGE_ONE = '/thread/1/'
21 24 THREAD_PAGE = '/thread/'
22 25 TAG_PAGE = '/tag/'
23 26 HTTP_CODE_REDIRECT = 302
24 27 HTTP_CODE_OK = 200
25 28 HTTP_CODE_NOT_FOUND = 404
26 29
27 30 logger = logging.getLogger(__name__)
28 31
29 32
30 33 class PostTests(TestCase):
31 34
32 35 def _create_post(self):
33 return Post.objects.create_post(title='title',
34 text='text')
36 tag = Tag.objects.create(name=TEST_TAG)
37 return Post.objects.create_post(title='title', text='text',
38 tags=[tag])
35 39
36 40 def test_post_add(self):
37 41 """Test adding post"""
38 42
39 43 post = self._create_post()
40 44
41 self.assertIsNotNone(post, 'No post was created')
45 self.assertIsNotNone(post, 'No post was created.')
46 self.assertEqual(TEST_TAG, post.get_thread().tags.all()[0].name,
47 'No tags were added to the post.')
42 48
43 49 def test_delete_post(self):
44 50 """Test post deletion"""
45 51
46 52 post = self._create_post()
47 53 post_id = post.id
48 54
49 55 Post.objects.delete_post(post)
50 56
51 57 self.assertFalse(Post.objects.filter(id=post_id).exists())
52 58
53 59 def test_delete_thread(self):
54 60 """Test thread deletion"""
55 61
56 62 opening_post = self._create_post()
57 63 thread = opening_post.get_thread()
58 64 reply = Post.objects.create_post("", "", thread=thread)
59 65
60 66 thread.delete()
61 67
62 68 self.assertFalse(Post.objects.filter(id=reply.id).exists())
63 69
64 70 def test_post_to_thread(self):
65 71 """Test adding post to a thread"""
66 72
67 73 op = self._create_post()
68 74 post = Post.objects.create_post("", "", thread=op.get_thread())
69 75
70 76 self.assertIsNotNone(post, 'Reply to thread wasn\'t created')
71 77 self.assertEqual(op.get_thread().last_edit_time, post.pub_time,
72 78 'Post\'s create time doesn\'t match thread last edit'
73 79 ' time')
74 80
75 81 def test_delete_posts_by_ip(self):
76 82 """Test deleting posts with the given ip"""
77 83
78 84 post = self._create_post()
79 85 post_id = post.id
80 86
81 87 Post.objects.delete_posts_by_ip('0.0.0.0')
82 88
83 89 self.assertFalse(Post.objects.filter(id=post_id).exists())
84 90
85 91 def test_get_thread(self):
86 92 """Test getting all posts of a thread"""
87 93
88 94 opening_post = self._create_post()
89 95
90 96 for i in range(0, 2):
91 97 Post.objects.create_post('title', 'text',
92 98 thread=opening_post.get_thread())
93 99
94 100 thread = opening_post.get_thread()
95 101
96 102 self.assertEqual(3, thread.replies.count())
97 103
98 104 def test_create_post_with_tag(self):
99 105 """Test adding tag to post"""
100 106
101 107 tag = Tag.objects.create(name='test_tag')
102 108 post = Post.objects.create_post(title='title', text='text', tags=[tag])
103 109
104 110 thread = post.get_thread()
105 111 self.assertIsNotNone(post, 'Post not created')
106 112 self.assertTrue(tag in thread.tags.all(), 'Tag not added to thread')
107 113 self.assertTrue(thread in tag.threads.all(), 'Thread not added to tag')
108 114
109 115 def test_thread_max_count(self):
110 116 """Test deletion of old posts when the max thread count is reached"""
111 117
112 118 for i in range(settings.MAX_THREAD_COUNT + 1):
113 119 self._create_post()
114 120
115 121 self.assertEqual(settings.MAX_THREAD_COUNT,
116 122 len(Thread.objects.filter(archived=False)))
117 123
118 124 def test_pages(self):
119 125 """Test that the thread list is properly split into pages"""
120 126
121 127 for i in range(settings.MAX_THREAD_COUNT):
122 128 self._create_post()
123 129
124 130 all_threads = Thread.objects.filter(archived=False)
125 131
126 132 paginator = Paginator(Thread.objects.filter(archived=False),
127 133 settings.THREADS_PER_PAGE)
128 134 posts_in_second_page = paginator.page(2).object_list
129 135 first_post = posts_in_second_page[0]
130 136
131 137 self.assertEqual(all_threads[settings.THREADS_PER_PAGE].id,
132 138 first_post.id)
133 139
134 def test_linked_tag(self):
135 """Test adding a linked tag"""
136
137 linked_tag = Tag.objects.create(name=u'tag1')
138 tag = Tag.objects.create(name=u'tag2', linked=linked_tag)
139
140 post = Post.objects.create_post("", "", tags=[tag])
141
142 self.assertTrue(linked_tag in post.get_thread().tags.all(),
143 'Linked tag was not added')
144
145 140
146 141 class PagesTest(TestCase):
147 142
148 143 def test_404(self):
149 144 """Test receiving error 404 when opening a non-existent page"""
150 145
151 146 tag_name = u'test_tag'
152 147 tag = Tag.objects.create(name=tag_name)
153 148 client = Client()
154 149
155 150 Post.objects.create_post('title', TEST_TEXT, tags=[tag])
156 151
157 152 existing_post_id = Post.objects.all()[0].id
158 153 response_existing = client.get(THREAD_PAGE + str(existing_post_id) +
159 154 '/')
160 155 self.assertEqual(HTTP_CODE_OK, response_existing.status_code,
161 156 u'Cannot open existing thread')
162 157
163 158 response_not_existing = client.get(THREAD_PAGE + str(
164 159 existing_post_id + 1) + '/')
165 160 self.assertEqual(PAGE_404, response_not_existing.templates[0].name,
166 161 u'Not existing thread is opened')
167 162
168 163 response_existing = client.get(TAG_PAGE + tag_name + '/')
169 164 self.assertEqual(HTTP_CODE_OK,
170 165 response_existing.status_code,
171 166 u'Cannot open existing tag')
172 167
173 168 response_not_existing = client.get(TAG_PAGE + u'not_tag' + '/')
174 169 self.assertEqual(PAGE_404,
175 170 response_not_existing.templates[0].name,
176 171 u'Not existing tag is opened')
177 172
178 173 reply_id = Post.objects.create_post('', TEST_TEXT,
179 174 thread=Post.objects.all()[0]
180 175 .get_thread())
181 176 response_not_existing = client.get(THREAD_PAGE + str(
182 177 reply_id) + '/')
183 178 self.assertEqual(PAGE_404,
184 179 response_not_existing.templates[0].name,
185 180 u'Reply is opened as a thread')
186 181
187 182
188 183 class FormTest(TestCase):
189 184 def test_post_validation(self):
190 185 # Disable captcha for the test
191 186 captcha_enabled = neboard.settings.ENABLE_CAPTCHA
192 187 neboard.settings.ENABLE_CAPTCHA = False
193 188
194 189 client = Client()
195 190
196 191 valid_tags = u'tag1 tag_2 тег_3'
197 192 invalid_tags = u'$%_356 ---'
198 193
199 194 response = client.post(NEW_THREAD_PAGE, {'title': 'test title',
200 195 'text': TEST_TEXT,
201 196 'tags': valid_tags})
202 197 self.assertEqual(response.status_code, HTTP_CODE_REDIRECT,
203 198 msg='Posting new message failed: got code ' +
204 199 str(response.status_code))
205 200
206 201 self.assertEqual(1, Post.objects.count(),
207 202 msg='No posts were created')
208 203
209 204 client.post(NEW_THREAD_PAGE, {'text': TEST_TEXT,
210 205 'tags': invalid_tags})
211 206 self.assertEqual(1, Post.objects.count(), msg='The validation passed '
212 207 'where it should fail')
213 208
214 209 # Change posting delay so we don't have to wait for 30 seconds or more
215 210 old_posting_delay = neboard.settings.POSTING_DELAY
216 211 # Wait fot the posting delay or we won't be able to post
217 212 settings.POSTING_DELAY = 1
218 213 time.sleep(neboard.settings.POSTING_DELAY + 1)
219 214 response = client.post(THREAD_PAGE_ONE, {'text': TEST_TEXT,
220 215 'tags': valid_tags})
221 216 self.assertEqual(HTTP_CODE_REDIRECT, response.status_code,
222 217 msg=u'Posting new message failed: got code ' +
223 218 str(response.status_code))
224 219 # Restore posting delay
225 220 settings.POSTING_DELAY = old_posting_delay
226 221
227 222 self.assertEqual(2, Post.objects.count(),
228 223 msg=u'No posts were created')
229 224
230 225 # Restore captcha setting
231 226 settings.ENABLE_CAPTCHA = captcha_enabled
232 227
233 228
234 229 class ViewTest(TestCase):
235 230
236 231 def test_all_views(self):
237 '''
232 """
238 233 Try opening all views defined in ulrs.py that don't need additional
239 234 parameters
240 '''
235 """
241 236
242 237 client = Client()
243 238 for url in urls.urlpatterns:
244 239 try:
245 240 view_name = url.name
246 241 logger.debug('Testing view %s' % view_name)
247 242
248 243 try:
249 244 response = client.get(reverse(view_name))
250 245
251 246 self.assertEqual(HTTP_CODE_OK, response.status_code,
252 247 '%s view not opened' % view_name)
253 248 except NoReverseMatch:
254 249 # This view just needs additional arguments
255 250 pass
256 251 except Exception, e:
257 252 self.fail('Got exception %s at %s view' % (e, view_name))
258 253 except AttributeError:
259 254 # This is normal, some views do not have names
260 255 pass
256
257
258 class AbstractTest(TestCase):
259 def test_settings_manager(self):
260 request = MockRequest()
261 settings_manager = get_settings_manager(request)
262
263 settings_manager.set_setting('test_setting', 'test_value')
264 self.assertEqual('test_value', settings_manager.get_setting(
265 'test_setting'), u'Setting update failed.')
266
267
268 class MockRequest:
269 def __init__(self):
270 self.session = dict()
@@ -1,83 +1,84 b''
1 1 from django.conf.urls import patterns, url, include
2 2 from boards import views
3 3 from boards.rss import AllThreadsFeed, TagThreadsFeed, ThreadPostsFeed
4 4 from boards.views import api, tag_threads, all_threads, \
5 login, settings, all_tags
5 login, settings, all_tags, logout
6 6 from boards.views.authors import AuthorsView
7 7 from boards.views.delete_post import DeletePostView
8 8 from boards.views.ban import BanUserView
9 9 from boards.views.search import BoardSearchView
10 10 from boards.views.static import StaticPageView
11 11 from boards.views.post_admin import PostAdminView
12 12
13 13 js_info_dict = {
14 14 'packages': ('boards',),
15 15 }
16 16
17 17 urlpatterns = patterns('',
18 18
19 19 # /boards/
20 20 url(r'^$', all_threads.AllThreadsView.as_view(), name='index'),
21 21 # /boards/page/
22 22 url(r'^page/(?P<page>\w+)/$', all_threads.AllThreadsView.as_view(),
23 23 name='index'),
24 24
25 25 # login page
26 26 url(r'^login/$', login.LoginView.as_view(), name='login'),
27 url(r'^logout/$', logout.LogoutView.as_view(), name='logout'),
27 28
28 29 # /boards/tag/tag_name/
29 30 url(r'^tag/(?P<tag_name>\w+)/$', tag_threads.TagView.as_view(),
30 31 name='tag'),
31 32 # /boards/tag/tag_id/page/
32 33 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/$',
33 34 tag_threads.TagView.as_view(), name='tag'),
34 35
35 36 # /boards/thread/
36 37 url(r'^thread/(?P<post_id>\w+)/$', views.thread.ThreadView.as_view(),
37 38 name='thread'),
38 39 url(r'^thread/(?P<post_id>\w+)/mode/(?P<mode>\w+)/$', views.thread.ThreadView
39 40 .as_view(), name='thread_mode'),
40 41
41 42 # /boards/post_admin/
42 43 url(r'^post_admin/(?P<post_id>\w+)/$', PostAdminView.as_view(),
43 44 name='post_admin'),
44 45
45 46 url(r'^settings/$', settings.SettingsView.as_view(), name='settings'),
46 47 url(r'^tags/$', all_tags.AllTagsView.as_view(), name='tags'),
47 48 url(r'^captcha/', include('captcha.urls')),
48 49 url(r'^authors/$', AuthorsView.as_view(), name='authors'),
49 50 url(r'^delete/(?P<post_id>\w+)/$', DeletePostView.as_view(),
50 51 name='delete'),
51 52 url(r'^ban/(?P<post_id>\w+)/$', BanUserView.as_view(), name='ban'),
52 53
53 54 url(r'^banned/$', views.banned.BannedView.as_view(), name='banned'),
54 55 url(r'^staticpage/(?P<name>\w+)/$', StaticPageView.as_view(),
55 56 name='staticpage'),
56 57
57 58 # RSS feeds
58 59 url(r'^rss/$', AllThreadsFeed()),
59 60 url(r'^page/(?P<page>\w+)/rss/$', AllThreadsFeed()),
60 61 url(r'^tag/(?P<tag_name>\w+)/rss/$', TagThreadsFeed()),
61 62 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/rss/$', TagThreadsFeed()),
62 63 url(r'^thread/(?P<post_id>\w+)/rss/$', ThreadPostsFeed()),
63 64
64 65 # i18n
65 66 url(r'^jsi18n/$', 'boards.views.cached_js_catalog', js_info_dict,
66 67 name='js_info_dict'),
67 68
68 69 # API
69 70 url(r'^api/post/(?P<post_id>\w+)/$', api.get_post, name="get_post"),
70 71 url(r'^api/diff_thread/(?P<thread_id>\w+)/(?P<last_update_time>\w+)/$',
71 72 api.api_get_threaddiff, name="get_thread_diff"),
72 73 url(r'^api/threads/(?P<count>\w+)/$', api.api_get_threads,
73 74 name='get_threads'),
74 75 url(r'^api/tags/$', api.api_get_tags, name='get_tags'),
75 76 url(r'^api/thread/(?P<opening_post_id>\w+)/$', api.api_get_thread_posts,
76 77 name='get_thread'),
77 78 url(r'^api/add_post/(?P<opening_post_id>\w+)/$', api.api_add_post,
78 79 name='add_post'),
79 80
80 81 # Search
81 82 url(r'^search/$', BoardSearchView.as_view(), name='search'),
82 83
83 84 )
@@ -1,129 +1,82 b''
1 1 """
2 2 This module contains helper functions and helper classes.
3 3 """
4 4 import hashlib
5 5 import time
6 6
7 7 from django.utils import timezone
8 import boards
9 8
10 9 from neboard import settings
11 from boards.models import User
12 from boards.models.user import RANK_USER
13 10
14 11
15 12 KEY_CAPTCHA_FAILS = 'key_captcha_fails'
16 13 KEY_CAPTCHA_DELAY_TIME = 'key_captcha_delay_time'
17 14 KEY_CAPTCHA_LAST_ACTIVITY = 'key_captcha_last_activity'
18 15
19 16
20 17 def need_include_captcha(request):
21 18 """
22 19 Check if request is made by a user.
23 20 It contains rules which check for bots.
24 21 """
25 22
26 23 if not settings.ENABLE_CAPTCHA:
27 24 return False
28 25
29 26 enable_captcha = False
30 27
31 28 #newcomer
32 29 if KEY_CAPTCHA_LAST_ACTIVITY not in request.session:
33 30 return settings.ENABLE_CAPTCHA
34 31
35 32 last_activity = request.session[KEY_CAPTCHA_LAST_ACTIVITY]
36 33 current_delay = int(time.time()) - last_activity
37 34
38 35 delay_time = (request.session[KEY_CAPTCHA_DELAY_TIME]
39 36 if KEY_CAPTCHA_DELAY_TIME in request.session
40 37 else settings.CAPTCHA_DEFAULT_SAFE_TIME)
41 38
42 39 if current_delay < delay_time:
43 40 enable_captcha = True
44 41
45 42 print 'ENABLING' + str(enable_captcha)
46 43
47 44 return enable_captcha
48 45
49 46
50 47 def update_captcha_access(request, passed):
51 48 """
52 49 Update captcha fields.
53 50 It will reduce delay time if user passed captcha verification and
54 51 it will increase it otherwise.
55 52 """
56 53 session = request.session
57 54
58 55 delay_time = (request.session[KEY_CAPTCHA_DELAY_TIME]
59 56 if KEY_CAPTCHA_DELAY_TIME in request.session
60 57 else settings.CAPTCHA_DEFAULT_SAFE_TIME)
61 58
62 59 print "DELAY TIME = " + str(delay_time)
63 60
64 61 if passed:
65 62 delay_time -= 2 if delay_time >= 7 else 5
66 63 else:
67 64 delay_time += 10
68 65
69 66 session[KEY_CAPTCHA_LAST_ACTIVITY] = int(time.time())
70 67 session[KEY_CAPTCHA_DELAY_TIME] = delay_time
71 68
72 69
73 70 def get_client_ip(request):
74 71 x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
75 72 if x_forwarded_for:
76 73 ip = x_forwarded_for.split(',')[-1].strip()
77 74 else:
78 75 ip = request.META.get('REMOTE_ADDR')
79 76 return ip
80 77
81 78
82 79 def datetime_to_epoch(datetime):
83 80 return int(time.mktime(timezone.localtime(
84 81 datetime,timezone.get_current_timezone()).timetuple())
85 * 1000000 + datetime.microsecond)
86
87
88 def get_user(request):
89 """
90 Get current user from the session. If the user does not exist, create
91 a new one.
92 """
93
94 session = request.session
95 if not 'user_id' in session:
96 request.session.save()
97
98 md5 = hashlib.md5()
99 md5.update(session.session_key)
100 new_id = md5.hexdigest()
101
102 while User.objects.filter(user_id=new_id).exists():
103 md5.update(str(timezone.now()))
104 new_id = md5.hexdigest()
105
106 time_now = timezone.now()
107 user = User.objects.create(user_id=new_id, rank=RANK_USER,
108 registration_time=time_now)
109
110 session['user_id'] = user.id
111 else:
112 user = User.objects.select_related('fav_tags').get(
113 id=session['user_id'])
114
115 return user
116
117
118 def get_theme(request, user=None):
119 """
120 Get user's CSS theme
121 """
122
123 if not user:
124 user = get_user(request)
125 theme = user.get_setting('theme')
126 if not theme:
127 theme = boards.settings.DEFAULT_THEME
128
129 return theme No newline at end of file
82 * 1000000 + datetime.microsecond) No newline at end of file
@@ -1,138 +1,139 b''
1 1 import string
2 2
3 3 from django.db import transaction
4 4 from django.shortcuts import render, redirect
5 5
6 6 from boards import utils, settings
7 7 from boards.abstracts.paginator import get_paginator
8 from boards.abstracts.settingsmanager import get_settings_manager
8 9 from boards.forms import ThreadForm, PlainErrorList
9 10 from boards.models import Post, Thread, Ban, Tag
10 11 from boards.views.banned import BannedView
11 12 from boards.views.base import BaseBoardView, CONTEXT_FORM
12 13 from boards.views.posting_mixin import PostMixin
13 14
14 15 FORM_TAGS = 'tags'
15 16 FORM_TEXT = 'text'
16 17 FORM_TITLE = 'title'
17 18 FORM_IMAGE = 'image'
18 19
19 20 TAG_DELIMITER = ' '
20 21
21 22 PARAMETER_CURRENT_PAGE = 'current_page'
22 23 PARAMETER_PAGINATOR = 'paginator'
23 24 PARAMETER_THREADS = 'threads'
24 25
25 26 TEMPLATE = 'boards/posting_general.html'
26 27 DEFAULT_PAGE = 1
27 28
28 29
29 30 class AllThreadsView(PostMixin, BaseBoardView):
30 31
31 user = None
32 def __init__(self):
33 self.settings_manager = None
34 super(AllThreadsView, self).__init__()
32 35
33 36 def get(self, request, page=DEFAULT_PAGE, form=None):
34 37 context = self.get_context_data(request=request)
35 38
36 self.user = utils.get_user(request)
37
38 39 if not form:
39 40 form = ThreadForm(error_class=PlainErrorList)
40 41
42 self.settings_manager = get_settings_manager(request)
41 43 paginator = get_paginator(self.get_threads(),
42 44 settings.THREADS_PER_PAGE)
43 45 paginator.current_page = int(page)
44 46
45 47 threads = paginator.page(page).object_list
46 48
47 49 context[PARAMETER_THREADS] = threads
48 50 context[CONTEXT_FORM] = form
49 51
50 52 self._get_page_context(paginator, context, page)
51 53
52 54 return render(request, TEMPLATE, context)
53 55
54 56 def post(self, request, page=DEFAULT_PAGE):
55 57 form = ThreadForm(request.POST, request.FILES,
56 58 error_class=PlainErrorList)
57 59 form.session = request.session
58 60
59 61 if form.is_valid():
60 62 return self.create_thread(request, form)
61 63 if form.need_to_ban:
62 64 # Ban user because he is suspected to be a bot
63 65 self._ban_current_user(request)
64 66
65 67 return self.get(request, page, form)
66 68
67 69 @staticmethod
68 70 def _get_page_context(paginator, context, page):
69 71 """
70 72 Get pagination context variables
71 73 """
72 74
73 75 context[PARAMETER_PAGINATOR] = paginator
74 76 context[PARAMETER_CURRENT_PAGE] = paginator.page(int(page))
75 77
76 78 @staticmethod
77 79 def parse_tags_string(tag_strings):
78 80 """
79 81 Parses tag list string and returns tag object list.
80 82 """
81 83
82 84 tags = []
83 85
84 86 if tag_strings:
85 87 tag_strings = tag_strings.split(TAG_DELIMITER)
86 88 for tag_name in tag_strings:
87 89 tag_name = string.lower(tag_name.strip())
88 90 if len(tag_name) > 0:
89 91 tag, created = Tag.objects.get_or_create(name=tag_name)
90 92 tags.append(tag)
91 93
92 94 return tags
93 95
94 96 @transaction.atomic
95 97 def create_thread(self, request, form, html_response=True):
96 98 """
97 99 Creates a new thread with an opening post.
98 100 """
99 101
100 102 ip = utils.get_client_ip(request)
101 103 is_banned = Ban.objects.filter(ip=ip).exists()
102 104
103 105 if is_banned:
104 106 if html_response:
105 107 return redirect(BannedView().as_view())
106 108 else:
107 109 return
108 110
109 111 data = form.cleaned_data
110 112
111 113 title = data[FORM_TITLE]
112 114 text = data[FORM_TEXT]
113 115
114 116 text = self._remove_invalid_links(text)
115 117
116 118 if FORM_IMAGE in data.keys():
117 119 image = data[FORM_IMAGE]
118 120 else:
119 121 image = None
120 122
121 123 tag_strings = data[FORM_TAGS]
122 124
123 125 tags = self.parse_tags_string(tag_strings)
124 126
125 post = Post.objects.create_post(title=title, text=text, ip=ip,
126 image=image, tags=tags,
127 user=utils.get_user(request))
127 post = Post.objects.create_post(title=title, text=text, image=image,
128 ip=ip, tags=tags)
128 129
129 130 if html_response:
130 131 return redirect(post.get_url())
131 132
132 133 def get_threads(self):
133 134 """
134 135 Gets list of threads that will be shown on a page.
135 136 """
136 137
137 138 return Thread.objects.all().order_by('-bump_time')\
138 .exclude(tags__in=self.user.hidden_tags.all())
139 .exclude(tags__in=self.settings_manager.get_hidden_tags())
@@ -1,24 +1,26 b''
1 1 from django.db import transaction
2 2 from django.shortcuts import get_object_or_404
3 from boards import utils
4 3
4 from boards.abstracts.settingsmanager import PERMISSION_MODERATE, \
5 get_settings_manager
5 6 from boards.views.base import BaseBoardView
6 7 from boards.models import Post, Ban
7 8 from boards.views.mixins import RedirectNextMixin
8 9
9 10
10 11 class BanUserView(BaseBoardView, RedirectNextMixin):
11 12
12 13 @transaction.atomic
13 14 def get(self, request, post_id):
14 user = utils.get_user(request)
15 15 post = get_object_or_404(Post, id=post_id)
16 16
17 if user.is_moderator():
17 settings_manager = get_settings_manager(request)
18
19 if settings_manager.has_permission(PERMISSION_MODERATE):
18 20 # TODO Show confirmation page before ban
19 21 ban, created = Ban.objects.get_or_create(ip=post.poster_ip)
20 22 if created:
21 23 ban.reason = 'Banned for post ' + str(post_id)
22 24 ban.save()
23 25
24 26 return self.redirect_to_next(request)
@@ -1,27 +1,28 b''
1 1 from django.shortcuts import redirect, get_object_or_404
2 2 from django.db import transaction
3 from boards import utils
4 3
4 from boards.abstracts.settingsmanager import PERMISSION_MODERATE,\
5 get_settings_manager
5 6 from boards.views.base import BaseBoardView
6 7 from boards.views.mixins import RedirectNextMixin
7 8 from boards.models import Post
8 9
9 10
10 11 class DeletePostView(BaseBoardView, RedirectNextMixin):
11 12
12 13 @transaction.atomic
13 14 def get(self, request, post_id):
14 user = utils.get_user(request)
15 15 post = get_object_or_404(Post, id=post_id)
16 16
17 17 opening_post = post.is_opening()
18 18
19 if user.is_moderator():
19 settings_manager = get_settings_manager(request)
20 if settings_manager.has_permission(PERMISSION_MODERATE):
20 21 # TODO Show confirmation page before deletion
21 22 Post.objects.delete_post(post)
22 23
23 24 if not opening_post:
24 25 thread = post.thread_new
25 26 return redirect('thread', post_id=thread.get_opening_post().id)
26 27 else:
27 28 return self.redirect_to_next(request)
@@ -1,30 +1,33 b''
1 1 from django.shortcuts import render, redirect
2
3 from boards.abstracts.settingsmanager import PERMISSION_MODERATE, \
4 get_settings_manager
2 5 from boards.forms import LoginForm, PlainErrorList
3 from boards.models import User
4 6 from boards.views.base import BaseBoardView, CONTEXT_FORM
5 7
8
6 9 __author__ = 'neko259'
7 10
8 11
9 12 class LoginView(BaseBoardView):
10 13
11 14 def get(self, request, form=None):
12 15 context = self.get_context_data(request=request)
13 16
14 17 if not form:
15 18 form = LoginForm()
16 19 context[CONTEXT_FORM] = form
17 20
18 21 return render(request, 'boards/login.html', context)
19 22
20 23 def post(self, request):
21 24 form = LoginForm(request.POST, request.FILES,
22 25 error_class=PlainErrorList)
23 26 form.session = request.session
24 27
25 28 if form.is_valid():
26 user = User.objects.get(user_id=form.cleaned_data['user_id'])
27 request.session['user_id'] = user.id
29 settings_manager = get_settings_manager(request)
30 settings_manager.add_permission(PERMISSION_MODERATE)
28 31 return redirect('index')
29 32 else:
30 33 return self.get(request, form)
@@ -1,57 +1,60 b''
1 1 from django.shortcuts import render, get_object_or_404, redirect
2 2
3 from boards.abstracts.settingsmanager import PERMISSION_MODERATE,\
4 get_settings_manager
3 5 from boards.views.base import BaseBoardView
4 6 from boards.views.mixins import DispatcherMixin
5 7 from boards.models.post import Post
6 8 from boards.models.tag import Tag
7 9 from boards.forms import AddTagForm, PlainErrorList
8 10
11
9 12 class PostAdminView(BaseBoardView, DispatcherMixin):
10 13
11 14 def get(self, request, post_id, form=None):
12 user = utils.get_user(request)
13 if not user.is_moderator:
15 settings_manager = get_settings_manager(request)
16 if not settings_manager.has_permission(PERMISSION_MODERATE):
14 17 redirect('index')
15 18
16 19 post = get_object_or_404(Post, id=post_id)
17 20
18 21 if not form:
19 22 dispatch_result = self.dispatch_method(request, post)
20 23 if dispatch_result:
21 24 return dispatch_result
22 25 form = AddTagForm()
23 26
24 27 context = self.get_context_data(request=request)
25 28
26 29 context['post'] = post
27 30
28 31 context['tag_form'] = form
29 32
30 33 return render(request, 'boards/post_admin.html', context)
31 34
32 35 def post(self, request, post_id):
33 user = utils.get_user(request)
34 if not user.is_moderator:
36 settings_manager = get_settings_manager(request)
37 if not settings_manager.has_permission(PERMISSION_MODERATE):
35 38 redirect('index')
36 39
37 40 post = get_object_or_404(Post, id=post_id)
38 41 return self.dispatch_method(request, post)
39 42
40 43 def delete_tag(self, request, post):
41 44 tag_name = request.GET['tag']
42 45 tag = get_object_or_404(Tag, name=tag_name)
43 46
44 47 post.remove_tag(tag)
45 48
46 49 return redirect('post_admin', post.id)
47 50
48 51 def add_tag(self, request, post):
49 52 form = AddTagForm(request.POST, error_class=PlainErrorList)
50 53 if form.is_valid():
51 54 tag_name = form.cleaned_data['tag']
52 55 tag, created = Tag.objects.get_or_create(name=tag_name)
53 56
54 57 post.add_tag(tag)
55 58 return redirect('post_admin', post.id)
56 59 else:
57 60 return self.get(request, post.id, form)
@@ -1,41 +1,40 b''
1 1 from django.shortcuts import render
2 2 from django.template import RequestContext
3 3 from django.views.generic import View
4 4 from haystack.query import SearchQuerySet
5 5 from boards.abstracts.paginator import get_paginator
6 6 from boards.forms import SearchForm, PlainErrorList
7 7
8 8 FORM_QUERY = 'query'
9 9
10 10 CONTEXT_QUERY = 'query'
11 11 CONTEXT_FORM = 'form'
12 12 CONTEXT_PAGE = 'page'
13 13
14 14 REQUEST_PAGE = 'page'
15 15
16 16 __author__ = 'neko259'
17 17
18 18 TEMPLATE = 'search/search.html'
19 19
20 20
21 21 class BoardSearchView(View):
22 22 def get(self, request):
23 23 context = RequestContext(request)
24 24 form = SearchForm(request.GET, error_class=PlainErrorList)
25 25 context[CONTEXT_FORM] = form
26 26
27 27 if form.is_valid():
28 28 query = form.cleaned_data[FORM_QUERY]
29 29 if len(query) >= 3:
30 results = SearchQuerySet().auto_query(query).order_by('-id') \
31 .highlight()
30 results = SearchQuerySet().auto_query(query).order_by('-id')
32 31 paginator = get_paginator(results, 10)
33 32
34 33 if REQUEST_PAGE in request.GET:
35 34 page = int(request.GET[REQUEST_PAGE])
36 35 else:
37 36 page = 1
38 37 context[CONTEXT_PAGE] = paginator.page(page)
39 38 context[CONTEXT_QUERY] = query
40 39
41 40 return render(request, TEMPLATE, context) No newline at end of file
@@ -1,53 +1,38 b''
1 1 from django.db import transaction
2 2 from django.shortcuts import render, redirect
3 from boards import utils
4 3
4 from boards.abstracts.settingsmanager import get_settings_manager
5 5 from boards.views.base import BaseBoardView, CONTEXT_FORM
6 from boards.forms import SettingsForm, ModeratorSettingsForm, PlainErrorList
7 from boards.models.post import SETTING_MODERATE
6 from boards.forms import SettingsForm, PlainErrorList
7
8 CONTEXT_HIDDEN_TAGS = 'hidden_tags'
8 9
9 10
10 11 class SettingsView(BaseBoardView):
11 12
12 13 def get(self, request):
13 14 context = self.get_context_data(request=request)
14 user = utils.get_user(request)
15 is_moderator = user.is_moderator()
16
17 selected_theme = utils.get_theme(request, user)
15 settings_manager = get_settings_manager(request)
18 16
19 if is_moderator:
20 form = ModeratorSettingsForm(initial={
21 'theme': selected_theme,
22 'moderate': user.get_setting(SETTING_MODERATE) and \
23 user.is_moderator()
24 }, error_class=PlainErrorList)
25 else:
17 selected_theme = settings_manager.get_theme()
18
26 19 form = SettingsForm(initial={'theme': selected_theme},
27 20 error_class=PlainErrorList)
28 21
29 22 context[CONTEXT_FORM] = form
23 context[CONTEXT_HIDDEN_TAGS] = settings_manager.get_hidden_tags()
30 24
31 25 return render(request, 'boards/settings.html', context)
32 26
33 27 def post(self, request):
34 user = utils.get_user(request)
35 is_moderator = user.is_moderator()
28 settings_manager = get_settings_manager(request)
36 29
37 30 with transaction.atomic():
38 if is_moderator:
39 form = ModeratorSettingsForm(request.POST,
40 error_class=PlainErrorList)
41 else:
42 31 form = SettingsForm(request.POST, error_class=PlainErrorList)
43 32
44 33 if form.is_valid():
45 34 selected_theme = form.cleaned_data['theme']
46 35
47 user.save_setting('theme', selected_theme)
48
49 if is_moderator:
50 moderate = form.cleaned_data['moderate']
51 user.save_setting(SETTING_MODERATE, moderate)
36 settings_manager.set_theme(selected_theme)
52 37
53 38 return redirect('settings')
@@ -1,87 +1,92 b''
1 1 from django.shortcuts import get_object_or_404
2 from boards import utils
3 from boards.models import Tag, Post
2
3 from boards.abstracts.settingsmanager import get_settings_manager
4 from boards.models import Tag
4 5 from boards.views.all_threads import AllThreadsView, DEFAULT_PAGE
5 6 from boards.views.mixins import DispatcherMixin, RedirectNextMixin
6 7 from boards.forms import ThreadForm, PlainErrorList
7 8
9
8 10 __author__ = 'neko259'
9 11
10 12
11 13 class TagView(AllThreadsView, DispatcherMixin, RedirectNextMixin):
12 14
13 15 tag_name = None
14 16
15 17 def get_threads(self):
16 18 tag = get_object_or_404(Tag, name=self.tag_name)
17 19
18 20 return tag.threads.all().order_by('-bump_time')
19 21
20 22 def get_context_data(self, **kwargs):
21 23 context = super(TagView, self).get_context_data(**kwargs)
22 24
25 settings_manager = get_settings_manager(kwargs['request'])
26
23 27 tag = get_object_or_404(Tag, name=self.tag_name)
24 28 context['tag'] = tag
25 29
30 context['fav_tags'] = settings_manager.get_fav_tags()
31 context['hidden_tags'] = settings_manager.get_hidden_tags()
32
26 33 return context
27 34
28 35 def get(self, request, tag_name, page=DEFAULT_PAGE, form=None):
29 36 self.tag_name = tag_name
30 37
31 38 dispatch_result = self.dispatch_method(request)
32 39 if dispatch_result:
33 40 return dispatch_result
34 41 else:
35 42 return super(TagView, self).get(request, page, form)
36 43
37 44 def post(self, request, tag_name, page=DEFAULT_PAGE):
38 45 form = ThreadForm(request.POST, request.FILES,
39 46 error_class=PlainErrorList)
40 47 form.session = request.session
41 48
42 49 if form.is_valid():
43 50 return self.create_thread(request, form)
44 51 if form.need_to_ban:
45 52 # Ban user because he is suspected to be a bot
46 53 self._ban_current_user(request)
47 54
48 55 return self.get(request, tag_name, page, form)
49 56
50 57 def subscribe(self, request):
51 user = utils.get_user(request)
52 58 tag = get_object_or_404(Tag, name=self.tag_name)
53 59
54 if not tag in user.fav_tags.all():
55 user.add_tag(tag)
60 settings_manager = get_settings_manager(request)
61 settings_manager.add_fav_tag(tag)
56 62
57 63 return self.redirect_to_next(request)
58 64
59 65 def unsubscribe(self, request):
60 user = utils.get_user(request)
61 66 tag = get_object_or_404(Tag, name=self.tag_name)
62 67
63 if tag in user.fav_tags.all():
64 user.remove_tag(tag)
68 settings_manager = get_settings_manager(request)
69 settings_manager.del_fav_tag(tag)
65 70
66 71 return self.redirect_to_next(request)
67 72
68 73 def hide(self, request):
69 74 """
70 75 Adds tag to user's hidden tags. Threads with this tag will not be
71 76 shown.
72 77 """
73 78
74 user = utils.get_user(request)
75 79 tag = get_object_or_404(Tag, name=self.tag_name)
76 80
77 user.hide_tag(tag)
81 settings_manager = get_settings_manager(request)
82 settings_manager.add_hidden_tag(tag)
78 83
79 84 def unhide(self, request):
80 85 """
81 86 Removed tag from user's hidden tags.
82 87 """
83 88
84 user = utils.get_user(request)
85 89 tag = get_object_or_404(Tag, name=self.tag_name)
86 90
87 user.unhide_tag(tag)
91 settings_manager = get_settings_manager(request)
92 settings_manager.del_hidden_tag(tag)
@@ -1,144 +1,142 b''
1 1 from django.core.urlresolvers import reverse
2 2 from django.db import transaction
3 3 from django.http import Http404
4 4 from django.shortcuts import get_object_or_404, render, redirect
5 5 from django.views.generic.edit import FormMixin
6 6
7 7 from boards import utils, settings
8 8 from boards.forms import PostForm, PlainErrorList
9 9 from boards.models import Post, Ban
10 10 from boards.views.banned import BannedView
11 11 from boards.views.base import BaseBoardView, CONTEXT_FORM
12 12 from boards.views.posting_mixin import PostMixin
13 13
14 14 TEMPLATE_GALLERY = 'boards/thread_gallery.html'
15 15 TEMPLATE_NORMAL = 'boards/thread.html'
16 16
17 17 CONTEXT_POSTS = 'posts'
18 18 CONTEXT_OP = 'opening_post'
19 19 CONTEXT_BUMPLIMIT_PRG = 'bumplimit_progress'
20 20 CONTEXT_POSTS_LEFT = 'posts_left'
21 21 CONTEXT_LASTUPDATE = "last_update"
22 22 CONTEXT_MAX_REPLIES = 'max_replies'
23 23 CONTEXT_THREAD = 'thread'
24 24 CONTEXT_BUMPABLE = 'bumpable'
25 25
26 26 FORM_TITLE = 'title'
27 27 FORM_TEXT = 'text'
28 28 FORM_IMAGE = 'image'
29 29
30 30 MODE_GALLERY = 'gallery'
31 31 MODE_NORMAL = 'normal'
32 32
33 33
34 34 class ThreadView(BaseBoardView, PostMixin, FormMixin):
35 35
36 36 def get(self, request, post_id, mode=MODE_NORMAL, form=None):
37 37 try:
38 38 opening_post = Post.objects.filter(id=post_id).only('thread_new')[0]
39 39 except IndexError:
40 40 raise Http404
41 41
42 42 # If this is not OP, don't show it as it is
43 43 if not opening_post or not opening_post.is_opening():
44 44 raise Http404
45 45
46 46 if not form:
47 47 form = PostForm(error_class=PlainErrorList)
48 48
49 49 thread_to_show = opening_post.get_thread()
50 50
51 51 context = self.get_context_data(request=request)
52 52
53 53 context[CONTEXT_FORM] = form
54 54 context[CONTEXT_LASTUPDATE] = utils.datetime_to_epoch(
55 55 thread_to_show.last_edit_time)
56 56 context[CONTEXT_THREAD] = thread_to_show
57 57 context[CONTEXT_MAX_REPLIES] = settings.MAX_POSTS_PER_THREAD
58 58
59 59 if MODE_NORMAL == mode:
60 60 bumpable = thread_to_show.can_bump()
61 61 context[CONTEXT_BUMPABLE] = bumpable
62 62 if bumpable:
63 63 left_posts = settings.MAX_POSTS_PER_THREAD \
64 64 - thread_to_show.get_reply_count()
65 65 context[CONTEXT_POSTS_LEFT] = left_posts
66 66 context[CONTEXT_BUMPLIMIT_PRG] = str(
67 67 float(left_posts) / settings.MAX_POSTS_PER_THREAD * 100)
68 68
69 69 context[CONTEXT_OP] = opening_post
70 70
71 71 document = TEMPLATE_NORMAL
72 72 elif MODE_GALLERY == mode:
73 73 context[CONTEXT_POSTS] = thread_to_show.get_replies_with_images(
74 74 view_fields_only=True)
75 75
76 76 document = TEMPLATE_GALLERY
77 77 else:
78 78 raise Http404
79 79
80 80 return render(request, document, context)
81 81
82 82 def post(self, request, post_id, mode=MODE_NORMAL):
83 83 opening_post = get_object_or_404(Post, id=post_id)
84 84
85 85 # If this is not OP, don't show it as it is
86 86 if not opening_post.is_opening():
87 87 raise Http404
88 88
89 89 if not opening_post.get_thread().archived:
90 90 form = PostForm(request.POST, request.FILES,
91 91 error_class=PlainErrorList)
92 92 form.session = request.session
93 93
94 94 if form.is_valid():
95 95 return self.new_post(request, form, opening_post)
96 96 if form.need_to_ban:
97 97 # Ban user because he is suspected to be a bot
98 98 self._ban_current_user(request)
99 99
100 100 return self.get(request, post_id, mode, form)
101 101
102 102 @transaction.atomic
103 103 def new_post(self, request, form, opening_post=None, html_response=True):
104 104 """Add a new post (in thread or as a reply)."""
105 105
106 106 ip = utils.get_client_ip(request)
107 107 is_banned = Ban.objects.filter(ip=ip).exists()
108 108
109 109 if is_banned:
110 110 if html_response:
111 111 return redirect(BannedView().as_view())
112 112 else:
113 113 return None
114 114
115 115 data = form.cleaned_data
116 116
117 117 title = data[FORM_TITLE]
118 118 text = data[FORM_TEXT]
119 119
120 120 text = self._remove_invalid_links(text)
121 121
122 122 if FORM_IMAGE in data.keys():
123 123 image = data[FORM_IMAGE]
124 124 else:
125 125 image = None
126 126
127 127 tags = []
128 128
129 129 post_thread = opening_post.get_thread()
130 130
131 post = Post.objects.create_post(title=title, text=text, ip=ip,
132 thread=post_thread, image=image,
133 tags=tags,
134 user=utils.get_user(request))
131 post = Post.objects.create_post(title=title, text=text, image=image,
132 thread=post_thread, ip=ip, tags=tags)
135 133
136 134 thread_to_show = (opening_post.id if opening_post else post.id)
137 135
138 136 if html_response:
139 137 if opening_post:
140 138 return redirect(
141 139 reverse('thread', kwargs={'post_id': thread_to_show})
142 140 + '#' + str(post.id))
143 141 else:
144 142 return post
@@ -1,37 +1,43 b''
1 1 # 1.5 Aker #
2 2 * Saving image previews size. No space will be shown below images in some
3 3 styles.
4 4 * Showing notification in page title when new posts are loaded into the open
5 5 thread.
6 6 * Thread moderation fixes
7 7 * Added new gallery with search links and image metadata
8 8
9 9 # 1.6 Amon #
10 10 * Deleted threads are moved to archive instead of permanent delete
11 11 * User management fixes and optimizations
12 12 * Markdown fixes
13 13 * Pagination changes. Pages counter now starts from 1 instead of 0
14 14 * Added API for viewing threads and posts
15 15 * New tag popularity algorithm
16 16 * Tags list page changes. Now tags list is more like a tag cloud
17 17
18 18 # 1.7 Anubis
19 19 * [ADMIN] Added admin page for post editing, capable of adding and removing tags
20 20 * [CODE] Post view unification
21 21 * Post caching instead of thread caching
22 22 * Simplified tag list page
23 23 * [API] Added api for thread update in json
24 24 * Image duplicate check
25 25 * Posting over ajax (no page reload now)
26 26 * Update last update time with thread update
27 27 * Added z-index to the images to move the dragged image to front
28 28 * [CODE] Major view refactoring. Now almost all views are class-based
29 29
30 30 # 1.8 Kara
31 31 * [CODE] Removed thread update logging
32 32 * [CODE] Refactored compact form. Now it uses the same one form and moves
33 33 elements instead of swapping them
34 34 * [CODE] Moved image to a separate model. This will allow to add multiple
35 35 images to a post
36 36 * Added search over posts and tags
37 37 * [ADMIN] Command to remove empty users
38
39 # 2.0 D'Anna
40 * Removed users. Now settings are stored in sessions
41 * Changed markdown to bbcode
42 * Removed linked tags
43 * [ADMIN] Added title to the post logs to make them more informative
@@ -1,260 +1,260 b''
1 1 # Django settings for neboard project.
2 2 import os
3 from boards.mdx_neboard import markdown_extended
3 from boards.mdx_neboard import bbcode_extended
4 4
5 5 DEBUG = True
6 6 TEMPLATE_DEBUG = DEBUG
7 7
8 8 ADMINS = (
9 9 # ('Your Name', 'your_email@example.com'),
10 10 ('admin', 'admin@example.com')
11 11 )
12 12
13 13 MANAGERS = ADMINS
14 14
15 15 DATABASES = {
16 16 'default': {
17 17 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
18 18 'NAME': 'database.db', # Or path to database file if using sqlite3.
19 19 'USER': '', # Not used with sqlite3.
20 20 'PASSWORD': '', # Not used with sqlite3.
21 21 'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
22 22 'PORT': '', # Set to empty string for default. Not used with sqlite3.
23 23 'CONN_MAX_AGE': None,
24 24 }
25 25 }
26 26
27 27 # Local time zone for this installation. Choices can be found here:
28 28 # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
29 29 # although not all choices may be available on all operating systems.
30 30 # In a Windows environment this must be set to your system time zone.
31 31 TIME_ZONE = 'Europe/Kiev'
32 32
33 33 # Language code for this installation. All choices can be found here:
34 34 # http://www.i18nguy.com/unicode/language-identifiers.html
35 35 LANGUAGE_CODE = 'en'
36 36
37 37 SITE_ID = 1
38 38
39 39 # If you set this to False, Django will make some optimizations so as not
40 40 # to load the internationalization machinery.
41 41 USE_I18N = True
42 42
43 43 # If you set this to False, Django will not format dates, numbers and
44 44 # calendars according to the current locale.
45 45 USE_L10N = True
46 46
47 47 # If you set this to False, Django will not use timezone-aware datetimes.
48 48 USE_TZ = True
49 49
50 50 # Absolute filesystem path to the directory that will hold user-uploaded files.
51 51 # Example: "/home/media/media.lawrence.com/media/"
52 52 MEDIA_ROOT = './media/'
53 53
54 54 # URL that handles the media served from MEDIA_ROOT. Make sure to use a
55 55 # trailing slash.
56 56 # Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
57 57 MEDIA_URL = '/media/'
58 58
59 59 # Absolute path to the directory static files should be collected to.
60 60 # Don't put anything in this directory yourself; store your static files
61 61 # in apps' "static/" subdirectories and in STATICFILES_DIRS.
62 62 # Example: "/home/media/media.lawrence.com/static/"
63 63 STATIC_ROOT = ''
64 64
65 65 # URL prefix for static files.
66 66 # Example: "http://media.lawrence.com/static/"
67 67 STATIC_URL = '/static/'
68 68
69 69 # Additional locations of static files
70 70 # It is really a hack, put real paths, not related
71 71 STATICFILES_DIRS = (
72 72 os.path.dirname(__file__) + '/boards/static',
73 73
74 74 # '/d/work/python/django/neboard/neboard/boards/static',
75 75 # Put strings here, like "/home/html/static" or "C:/www/django/static".
76 76 # Always use forward slashes, even on Windows.
77 77 # Don't forget to use absolute paths, not relative paths.
78 78 )
79 79
80 80 # List of finder classes that know how to find static files in
81 81 # various locations.
82 82 STATICFILES_FINDERS = (
83 83 'django.contrib.staticfiles.finders.FileSystemFinder',
84 84 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
85 85 )
86 86
87 87 if DEBUG:
88 88 STATICFILES_STORAGE = \
89 89 'django.contrib.staticfiles.storage.StaticFilesStorage'
90 90 else:
91 91 STATICFILES_STORAGE = \
92 92 'django.contrib.staticfiles.storage.CachedStaticFilesStorage'
93 93
94 94 # Make this unique, and don't share it with anybody.
95 95 SECRET_KEY = '@1rc$o(7=tt#kd+4s$u6wchm**z^)4x90)7f6z(i&amp;55@o11*8o'
96 96
97 97 # List of callables that know how to import templates from various sources.
98 98 TEMPLATE_LOADERS = (
99 99 'django.template.loaders.filesystem.Loader',
100 100 'django.template.loaders.app_directories.Loader',
101 101 )
102 102
103 103 TEMPLATE_CONTEXT_PROCESSORS = (
104 104 'django.core.context_processors.media',
105 105 'django.core.context_processors.static',
106 106 'django.core.context_processors.request',
107 107 'django.contrib.auth.context_processors.auth',
108 108 'boards.context_processors.user_and_ui_processor',
109 109 )
110 110
111 111 MIDDLEWARE_CLASSES = (
112 112 'django.contrib.sessions.middleware.SessionMiddleware',
113 113 'django.middleware.locale.LocaleMiddleware',
114 114 'django.middleware.common.CommonMiddleware',
115 115 'django.contrib.auth.middleware.AuthenticationMiddleware',
116 116 'django.contrib.messages.middleware.MessageMiddleware',
117 117 'boards.middlewares.BanMiddleware',
118 118 'boards.middlewares.MinifyHTMLMiddleware',
119 119 )
120 120
121 121 ROOT_URLCONF = 'neboard.urls'
122 122
123 123 # Python dotted path to the WSGI application used by Django's runserver.
124 124 WSGI_APPLICATION = 'neboard.wsgi.application'
125 125
126 126 TEMPLATE_DIRS = (
127 127 # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
128 128 # Always use forward slashes, even on Windows.
129 129 # Don't forget to use absolute paths, not relative paths.
130 130 'templates',
131 131 )
132 132
133 133 INSTALLED_APPS = (
134 134 'django.contrib.auth',
135 135 'django.contrib.contenttypes',
136 136 'django.contrib.sessions',
137 137 # 'django.contrib.sites',
138 138 'django.contrib.messages',
139 139 'django.contrib.staticfiles',
140 140 # Uncomment the next line to enable the admin:
141 141 'django.contrib.admin',
142 142 # Uncomment the next line to enable admin documentation:
143 143 # 'django.contrib.admindocs',
144 144 'django.contrib.humanize',
145 145 'django_cleanup',
146 146
147 147 # Migrations
148 148 'south',
149 149 'debug_toolbar',
150 150
151 151 'captcha',
152 152
153 153 # Search
154 154 'haystack',
155 155
156 156 'boards',
157 157 )
158 158
159 159 DEBUG_TOOLBAR_PANELS = (
160 160 'debug_toolbar.panels.version.VersionDebugPanel',
161 161 'debug_toolbar.panels.timer.TimerDebugPanel',
162 162 'debug_toolbar.panels.settings_vars.SettingsVarsDebugPanel',
163 163 'debug_toolbar.panels.headers.HeaderDebugPanel',
164 164 'debug_toolbar.panels.request_vars.RequestVarsDebugPanel',
165 165 'debug_toolbar.panels.template.TemplateDebugPanel',
166 166 'debug_toolbar.panels.sql.SQLDebugPanel',
167 167 'debug_toolbar.panels.signals.SignalDebugPanel',
168 168 'debug_toolbar.panels.logger.LoggingPanel',
169 169 )
170 170
171 171 # TODO: NEED DESIGN FIXES
172 172 CAPTCHA_OUTPUT_FORMAT = (u' %(hidden_field)s '
173 173 u'<div class="form-label">%(image)s</div>'
174 174 u'<div class="form-text">%(text_field)s</div>')
175 175
176 176 # A sample logging configuration. The only tangible logging
177 177 # performed by this configuration is to send an email to
178 178 # the site admins on every HTTP 500 error when DEBUG=False.
179 179 # See http://docs.djangoproject.com/en/dev/topics/logging for
180 180 # more details on how to customize your logging configuration.
181 181 LOGGING = {
182 182 'version': 1,
183 183 'disable_existing_loggers': False,
184 184 'formatters': {
185 185 'verbose': {
186 186 'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s'
187 187 },
188 188 'simple': {
189 189 'format': '%(levelname)s %(asctime)s [%(module)s] %(message)s'
190 190 },
191 191 },
192 192 'filters': {
193 193 'require_debug_false': {
194 194 '()': 'django.utils.log.RequireDebugFalse'
195 195 }
196 196 },
197 197 'handlers': {
198 198 'console': {
199 199 'level': 'DEBUG',
200 200 'class': 'logging.StreamHandler',
201 201 'formatter': 'simple'
202 202 },
203 203 },
204 204 'loggers': {
205 205 'boards': {
206 206 'handlers': ['console'],
207 207 'level': 'DEBUG',
208 208 }
209 209 },
210 210 }
211 211
212 212 HAYSTACK_CONNECTIONS = {
213 213 'default': {
214 214 'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine',
215 215 'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'),
216 216 },
217 217 }
218 218
219 219 MARKUP_FIELD_TYPES = (
220 ('markdown', markdown_extended),
220 ('bbcode', bbcode_extended),
221 221 )
222 222
223 223 THEMES = [
224 224 ('md', 'Mystic Dark'),
225 225 ('md_centered', 'Mystic Dark (centered)'),
226 226 ('sw', 'Snow White'),
227 227 ('pg', 'Photon Gray'),
228 228 ]
229 229
230 230 POPULAR_TAGS = 10
231 231
232 232 ENABLE_CAPTCHA = False
233 233 # if user tries to post before CAPTCHA_DEFAULT_SAFE_TIME. Captcha will be shown
234 234 CAPTCHA_DEFAULT_SAFE_TIME = 30 # seconds
235 235 POSTING_DELAY = 20 # seconds
236 236
237 237 COMPRESS_HTML = True
238 238
239 239 # Debug mode middlewares
240 240 if DEBUG:
241 241 MIDDLEWARE_CLASSES += (
242 242 'boards.profiler.ProfilerMiddleware',
243 243 'debug_toolbar.middleware.DebugToolbarMiddleware',
244 244 )
245 245
246 246 def custom_show_toolbar(request):
247 247 return DEBUG
248 248
249 249 DEBUG_TOOLBAR_CONFIG = {
250 250 'INTERCEPT_REDIRECTS': False,
251 251 'SHOW_TOOLBAR_CALLBACK': custom_show_toolbar,
252 252 'HIDE_DJANGO_SQL': False,
253 253 'ENABLE_STACKTRACES': True,
254 254 }
255 255
256 256 # FIXME Uncommenting this fails somehow. Need to investigate this
257 257 #DEBUG_TOOLBAR_PANELS += (
258 258 # 'debug_toolbar.panels.profiling.ProfilingDebugPanel',
259 259 #)
260 260
@@ -1,10 +1,10 b''
1 south>=0.8.4
1 2 line_profiler
2 3 haystack
3 4 pillow
4 5 django>=1.6
5 6 django_cleanup
6 7 django-markupfield
7 markdown
8 python-markdown
9 8 django-simple-captcha
10 9 line-profiler
10 bbcode
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now