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