##// END OF EJS Templates
Merged 1.8-dev into default
neko259 -
r704:7f7c33ba merge 1.8 default
parent child Browse files
Show More
@@ -0,0 +1,31 b''
1 from boards import utils
2 from boards.models import Post
3 from boards.models.post import SETTING_MODERATE
4 import neboard
5
6 __author__ = 'neko259'
7
8
9 def user_and_ui_processor(request):
10 context = {}
11
12 user = utils.get_user(request)
13 context['user'] = user
14 context['tags'] = user.fav_tags.all()
15 context['posts_per_day'] = float(Post.objects.get_posts_per_day())
16
17 theme = utils.get_theme(request, user)
18 context['theme'] = theme
19 context['theme_css'] = 'css/' + theme + '/base_page.css'
20
21 # This shows the moderator panel
22 moderate = user.get_setting(SETTING_MODERATE)
23 if moderate == 'True':
24 context['moderator'] = user.is_moderator()
25 else:
26 context['moderator'] = False
27
28 context['version'] = neboard.settings.VERSION
29 context['site_name'] = neboard.settings.SITE_NAME
30
31 return context No newline at end of file
@@ -0,0 +1,1 b''
1 __author__ = 'vurdalak'
@@ -0,0 +1,1 b''
1 __author__ = 'vurdalak'
@@ -0,0 +1,29 b''
1 from datetime import datetime, timedelta
2 from django.core.management import BaseCommand
3 from django.db import transaction
4 from django.db.models import Count
5 from boards.models import User, Post
6
7 __author__ = 'neko259'
8
9 OLD_USER_AGE_DAYS = 90
10
11
12 class Command(BaseCommand):
13 help = 'Removes empty users (that don\'t have posts or tags'
14
15 @transaction.atomic
16 def handle(self, *args, **options):
17 old_registration_date = datetime.now().date() - timedelta(
18 OLD_USER_AGE_DAYS)
19
20 old_users = User.objects.annotate(tags_count=Count('fav_tags')).filter(
21 tags_count=0).filter(registration_time__lt=old_registration_date)
22 deleted_users = 0
23 for user in old_users:
24 if not Post.objects.filter(user=user).exists():
25 self.stdout.write('Deleting user %s' % user.user_id)
26 user.delete()
27 deleted_users += 1
28
29 self.stdout.write('Deleted %d users' % deleted_users) No newline at end of file
@@ -0,0 +1,118 b''
1 # -*- coding: utf-8 -*-
2 from south.utils import datetime_utils as datetime
3 from south.db import db
4 from south.v2 import SchemaMigration
5 from django.db import models
6
7
8 class Migration(SchemaMigration):
9
10 def forwards(self, orm):
11 # Adding model 'PostImage'
12 db.create_table(u'boards_postimage', (
13 (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
14 ('width', self.gf('django.db.models.fields.IntegerField')(default=0)),
15 ('height', self.gf('django.db.models.fields.IntegerField')(default=0)),
16 ('pre_width', self.gf('django.db.models.fields.IntegerField')(default=0)),
17 ('pre_height', self.gf('django.db.models.fields.IntegerField')(default=0)),
18 ('image', self.gf('boards.thumbs.ImageWithThumbsField')(max_length=100, blank=True)),
19 ('hash', self.gf('django.db.models.fields.CharField')(max_length=36)),
20 ))
21 db.send_create_signal('boards', ['PostImage'])
22
23 # Adding M2M table for field images on 'Post'
24 m2m_table_name = db.shorten_name(u'boards_post_images')
25 db.create_table(m2m_table_name, (
26 ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
27 ('post', models.ForeignKey(orm['boards.post'], null=False)),
28 ('postimage', models.ForeignKey(orm['boards.postimage'], null=False))
29 ))
30 db.create_unique(m2m_table_name, ['post_id', 'postimage_id'])
31
32
33 def backwards(self, orm):
34 # Deleting model 'PostImage'
35 db.delete_table(u'boards_postimage')
36
37 # Removing M2M table for field images on 'Post'
38 db.delete_table(db.shorten_name(u'boards_post_images'))
39
40
41 models = {
42 'boards.ban': {
43 'Meta': {'object_name': 'Ban'},
44 'can_read': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
45 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
46 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
47 'reason': ('django.db.models.fields.CharField', [], {'default': "'Auto'", 'max_length': '200'})
48 },
49 'boards.post': {
50 'Meta': {'ordering': "('id',)", 'object_name': 'Post'},
51 '_text_rendered': ('django.db.models.fields.TextField', [], {}),
52 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
53 'image': ('boards.thumbs.ImageWithThumbsField', [], {'max_length': '100', 'blank': 'True'}),
54 'image_hash': ('django.db.models.fields.CharField', [], {'max_length': '36'}),
55 'image_height': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
56 'image_pre_height': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
57 'image_pre_width': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
58 'image_width': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
59 'images': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'ip+'", 'to': "orm['boards.PostImage']", 'blank': 'True', 'symmetrical': 'False', 'null': 'True', 'db_index': 'True'}),
60 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}),
61 'poster_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
62 'poster_user_agent': ('django.db.models.fields.TextField', [], {}),
63 'pub_time': ('django.db.models.fields.DateTimeField', [], {}),
64 'referenced_posts': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'rfp+'", 'to': "orm['boards.Post']", 'blank': 'True', 'symmetrical': 'False', 'null': 'True', 'db_index': 'True'}),
65 'refmap': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
66 'text': ('markupfield.fields.MarkupField', [], {'rendered_field': 'True'}),
67 'text_markup_type': ('django.db.models.fields.CharField', [], {'default': "'markdown'", 'max_length': '30'}),
68 'thread_new': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.Thread']", 'null': 'True'}),
69 'title': ('django.db.models.fields.CharField', [], {'max_length': '200'}),
70 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.User']", 'null': 'True'})
71 },
72 'boards.postimage': {
73 'Meta': {'ordering': "('id',)", 'object_name': 'PostImage'},
74 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
75 'image': ('boards.thumbs.ImageWithThumbsField', [], {'max_length': '100', 'blank': 'True'}),
76 'hash': ('django.db.models.fields.CharField', [], {'max_length': '36'}),
77 'height': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
78 'pre_height': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
79 'pre_width': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
80 'width': ('django.db.models.fields.IntegerField', [], {'default': '0'})
81 },
82 'boards.setting': {
83 'Meta': {'object_name': 'Setting'},
84 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
85 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
86 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['boards.User']"}),
87 'value': ('django.db.models.fields.CharField', [], {'max_length': '50'})
88 },
89 'boards.tag': {
90 'Meta': {'ordering': "('name',)", 'object_name': 'Tag'},
91 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
92 'linked': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['boards.Tag']", 'null': 'True', 'blank': 'True'}),
93 'name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}),
94 'threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'tag+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Thread']"})
95 },
96 'boards.thread': {
97 'Meta': {'object_name': 'Thread'},
98 'archived': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
99 'bump_time': ('django.db.models.fields.DateTimeField', [], {}),
100 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
101 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}),
102 'replies': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'tre+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
103 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['boards.Tag']", 'symmetrical': 'False'})
104 },
105 'boards.user': {
106 'Meta': {'object_name': 'User'},
107 'fav_tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['boards.Tag']", 'null': 'True', 'blank': 'True'}),
108 'fav_threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
109 'hidden_tags': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'ht+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Tag']"}),
110 'hidden_threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'hth+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
111 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
112 'rank': ('django.db.models.fields.IntegerField', [], {}),
113 'registration_time': ('django.db.models.fields.DateTimeField', [], {}),
114 'user_id': ('django.db.models.fields.CharField', [], {'max_length': '50'})
115 }
116 }
117
118 complete_apps = ['boards'] No newline at end of file
@@ -0,0 +1,110 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 DataMigration
5 from django.db import models
6
7
8 class Migration(DataMigration):
9
10 def forwards(self, orm):
11 for post in orm['boards.Post'].objects.all():
12 if post.image:
13 image = orm['boards.PostImage']()
14
15 image.width = post.image_width
16 image.height = post.image_height
17
18 image.pre_width = post.image_pre_width
19 image.pre_height = post.image_pre_height
20
21 image.image = post.image
22 image.hash = post.image_hash
23
24 image.save()
25
26 post.images.add(image)
27 post.save()
28
29 def backwards(self, orm):
30 "Write your backwards methods here."
31
32 models = {
33 'boards.ban': {
34 'Meta': {'object_name': 'Ban'},
35 'can_read': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
36 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
37 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
38 'reason': ('django.db.models.fields.CharField', [], {'default': "'Auto'", 'max_length': '200'})
39 },
40 'boards.post': {
41 'Meta': {'ordering': "('id',)", 'object_name': 'Post'},
42 '_text_rendered': ('django.db.models.fields.TextField', [], {}),
43 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
44 'image': ('boards.thumbs.ImageWithThumbsField', [], {'max_length': '100', 'blank': 'True'}),
45 'image_hash': ('django.db.models.fields.CharField', [], {'max_length': '36'}),
46 'image_height': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
47 'image_pre_height': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
48 'image_pre_width': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
49 'image_width': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
50 'images': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'ip+'", 'to': "orm['boards.PostImage']", 'blank': 'True', 'symmetrical': 'False', 'null': 'True', 'db_index': 'True'}),
51 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}),
52 'poster_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
53 'poster_user_agent': ('django.db.models.fields.TextField', [], {}),
54 'pub_time': ('django.db.models.fields.DateTimeField', [], {}),
55 'referenced_posts': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'rfp+'", 'to': "orm['boards.Post']", 'blank': 'True', 'symmetrical': 'False', 'null': 'True', 'db_index': 'True'}),
56 'refmap': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
57 'text': ('markupfield.fields.MarkupField', [], {'rendered_field': 'True'}),
58 'text_markup_type': ('django.db.models.fields.CharField', [], {'default': "'markdown'", 'max_length': '30'}),
59 'thread_new': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.Thread']", 'null': 'True'}),
60 'title': ('django.db.models.fields.CharField', [], {'max_length': '200'}),
61 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.User']", 'null': 'True'})
62 },
63 'boards.postimage': {
64 'Meta': {'ordering': "('id',)", 'object_name': 'PostImage'},
65 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
66 'image': ('boards.thumbs.ImageWithThumbsField', [], {'max_length': '100', 'blank': 'True'}),
67 'hash': ('django.db.models.fields.CharField', [], {'max_length': '36'}),
68 'height': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
69 'pre_height': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
70 'pre_width': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
71 'width': ('django.db.models.fields.IntegerField', [], {'default': '0'})
72 },
73 'boards.setting': {
74 'Meta': {'object_name': 'Setting'},
75 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
76 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
77 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['boards.User']"}),
78 'value': ('django.db.models.fields.CharField', [], {'max_length': '50'})
79 },
80 'boards.tag': {
81 'Meta': {'ordering': "('name',)", 'object_name': 'Tag'},
82 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
83 'linked': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['boards.Tag']", 'null': 'True', 'blank': 'True'}),
84 'name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}),
85 'threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'tag+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Thread']"})
86 },
87 'boards.thread': {
88 'Meta': {'object_name': 'Thread'},
89 'archived': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
90 'bump_time': ('django.db.models.fields.DateTimeField', [], {}),
91 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
92 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}),
93 'replies': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'tre+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
94 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['boards.Tag']", 'symmetrical': 'False'})
95 },
96 'boards.user': {
97 'Meta': {'object_name': 'User'},
98 'fav_tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['boards.Tag']", 'null': 'True', 'blank': 'True'}),
99 'fav_threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
100 'hidden_tags': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'ht+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Tag']"}),
101 'hidden_threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'hth+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
102 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
103 'rank': ('django.db.models.fields.IntegerField', [], {}),
104 'registration_time': ('django.db.models.fields.DateTimeField', [], {}),
105 'user_id': ('django.db.models.fields.CharField', [], {'max_length': '50'})
106 }
107 }
108
109 complete_apps = ['boards']
110 symmetrical = True
@@ -0,0 +1,141 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 'Post.image_pre_height'
12 db.delete_column(u'boards_post', 'image_pre_height')
13
14 # Deleting field 'Post.image'
15 db.delete_column(u'boards_post', 'image')
16
17 # Deleting field 'Post.image_pre_width'
18 db.delete_column(u'boards_post', 'image_pre_width')
19
20 # Deleting field 'Post.image_width'
21 db.delete_column(u'boards_post', 'image_width')
22
23 # Deleting field 'Post.image_height'
24 db.delete_column(u'boards_post', 'image_height')
25
26 # Deleting field 'Post.image_hash'
27 db.delete_column(u'boards_post', 'image_hash')
28
29
30 def backwards(self, orm):
31 # Adding field 'Post.image_pre_height'
32 db.add_column(u'boards_post', 'image_pre_height',
33 self.gf('django.db.models.fields.IntegerField')(default=0),
34 keep_default=False)
35
36
37 # User chose to not deal with backwards NULL issues for 'Post.image'
38 raise RuntimeError("Cannot reverse this migration. 'Post.image' and its values cannot be restored.")
39
40 # The following code is provided here to aid in writing a correct migration # Adding field 'Post.image'
41 db.add_column(u'boards_post', 'image',
42 self.gf('boards.thumbs.ImageWithThumbsField')(max_length=100, blank=True),
43 keep_default=False)
44
45 # Adding field 'Post.image_pre_width'
46 db.add_column(u'boards_post', 'image_pre_width',
47 self.gf('django.db.models.fields.IntegerField')(default=0),
48 keep_default=False)
49
50 # Adding field 'Post.image_width'
51 db.add_column(u'boards_post', 'image_width',
52 self.gf('django.db.models.fields.IntegerField')(default=0),
53 keep_default=False)
54
55 # Adding field 'Post.image_height'
56 db.add_column(u'boards_post', 'image_height',
57 self.gf('django.db.models.fields.IntegerField')(default=0),
58 keep_default=False)
59
60
61 # User chose to not deal with backwards NULL issues for 'Post.image_hash'
62 raise RuntimeError("Cannot reverse this migration. 'Post.image_hash' and its values cannot be restored.")
63
64 # The following code is provided here to aid in writing a correct migration # Adding field 'Post.image_hash'
65 db.add_column(u'boards_post', 'image_hash',
66 self.gf('django.db.models.fields.CharField')(max_length=36),
67 keep_default=False)
68
69
70 models = {
71 'boards.ban': {
72 'Meta': {'object_name': 'Ban'},
73 'can_read': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
74 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
75 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
76 'reason': ('django.db.models.fields.CharField', [], {'default': "'Auto'", 'max_length': '200'})
77 },
78 'boards.post': {
79 'Meta': {'ordering': "('id',)", 'object_name': 'Post'},
80 '_text_rendered': ('django.db.models.fields.TextField', [], {}),
81 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
82 'images': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'ip+'", 'to': "orm['boards.PostImage']", 'blank': 'True', 'symmetrical': 'False', 'null': 'True', 'db_index': 'True'}),
83 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}),
84 'poster_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
85 'poster_user_agent': ('django.db.models.fields.TextField', [], {}),
86 'pub_time': ('django.db.models.fields.DateTimeField', [], {}),
87 'referenced_posts': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'rfp+'", 'to': "orm['boards.Post']", 'blank': 'True', 'symmetrical': 'False', 'null': 'True', 'db_index': 'True'}),
88 'refmap': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
89 'text': ('markupfield.fields.MarkupField', [], {'rendered_field': 'True'}),
90 'text_markup_type': ('django.db.models.fields.CharField', [], {'default': "'markdown'", 'max_length': '30'}),
91 'thread_new': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.Thread']", 'null': 'True'}),
92 'title': ('django.db.models.fields.CharField', [], {'max_length': '200'}),
93 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['boards.User']", 'null': 'True'})
94 },
95 'boards.postimage': {
96 'Meta': {'ordering': "('id',)", 'object_name': 'PostImage'},
97 'hash': ('django.db.models.fields.CharField', [], {'max_length': '36'}),
98 'height': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
99 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
100 'image': ('boards.thumbs.ImageWithThumbsField', [], {'max_length': '100', 'blank': 'True'}),
101 'pre_height': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
102 'pre_width': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
103 'width': ('django.db.models.fields.IntegerField', [], {'default': '0'})
104 },
105 'boards.setting': {
106 'Meta': {'object_name': 'Setting'},
107 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
108 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
109 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['boards.User']"}),
110 'value': ('django.db.models.fields.CharField', [], {'max_length': '50'})
111 },
112 'boards.tag': {
113 'Meta': {'ordering': "('name',)", 'object_name': 'Tag'},
114 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
115 'linked': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['boards.Tag']", 'null': 'True', 'blank': 'True'}),
116 'name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}),
117 'threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'tag+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Thread']"})
118 },
119 'boards.thread': {
120 'Meta': {'object_name': 'Thread'},
121 'archived': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
122 'bump_time': ('django.db.models.fields.DateTimeField', [], {}),
123 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
124 'last_edit_time': ('django.db.models.fields.DateTimeField', [], {}),
125 'replies': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'tre+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
126 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['boards.Tag']", 'symmetrical': 'False'})
127 },
128 'boards.user': {
129 'Meta': {'object_name': 'User'},
130 'fav_tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['boards.Tag']", 'null': 'True', 'blank': 'True'}),
131 'fav_threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
132 'hidden_tags': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'ht+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Tag']"}),
133 'hidden_threads': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'hth+'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['boards.Post']"}),
134 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
135 'rank': ('django.db.models.fields.IntegerField', [], {}),
136 'registration_time': ('django.db.models.fields.DateTimeField', [], {}),
137 'user_id': ('django.db.models.fields.CharField', [], {'max_length': '50'})
138 }
139 }
140
141 complete_apps = ['boards'] No newline at end of file
@@ -0,0 +1,10 b''
1 __author__ = 'neko259'
2
3
4 class Viewable():
5 def __init__(self):
6 pass
7
8 def get_view(self, *args, **kwargs):
9 """Get an HTML view for a model"""
10 pass No newline at end of file
@@ -0,0 +1,60 b''
1 import hashlib
2 import os
3 from random import random
4 import time
5 from django.db import models
6 from boards import thumbs
7
8 __author__ = 'neko259'
9
10
11 IMAGE_THUMB_SIZE = (200, 150)
12 IMAGES_DIRECTORY = 'images/'
13 FILE_EXTENSION_DELIMITER = '.'
14
15
16 class PostImage(models.Model):
17 class Meta:
18 app_label = 'boards'
19 ordering = ('id',)
20
21 @staticmethod
22 def _update_image_filename(filename):
23 """
24 Gets unique image filename
25 """
26
27 path = IMAGES_DIRECTORY
28 new_name = str(int(time.mktime(time.gmtime())))
29 new_name += str(int(random() * 1000))
30 new_name += FILE_EXTENSION_DELIMITER
31 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
32
33 return os.path.join(path, new_name)
34
35 width = models.IntegerField(default=0)
36 height = models.IntegerField(default=0)
37
38 pre_width = models.IntegerField(default=0)
39 pre_height = models.IntegerField(default=0)
40
41 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
42 blank=True, sizes=(IMAGE_THUMB_SIZE,),
43 width_field='width',
44 height_field='height',
45 preview_width_field='pre_width',
46 preview_height_field='pre_height')
47 hash = models.CharField(max_length=36)
48
49 def save(self, *args, **kwargs):
50 """
51 Saves the model and computes the image hash for deduplication purposes.
52 """
53
54 if not self.pk and self.image:
55 md5 = hashlib.md5()
56 for chunk in self.image.chunks():
57 md5.update(chunk)
58 self.hash = md5.hexdigest()
59 super(PostImage, self).save(*args, **kwargs)
60
@@ -0,0 +1,163 b''
1 import logging
2 from django.db.models import Count
3 from django.utils import timezone
4 from django.core.cache import cache
5 from django.db import models
6 from neboard import settings
7
8 __author__ = 'neko259'
9
10
11 logger = logging.getLogger(__name__)
12
13
14 CACHE_KEY_OPENING_POST = 'opening_post_id'
15
16
17 class Thread(models.Model):
18
19 class Meta:
20 app_label = 'boards'
21
22 tags = models.ManyToManyField('Tag')
23 bump_time = models.DateTimeField()
24 last_edit_time = models.DateTimeField()
25 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
26 blank=True, related_name='tre+')
27 archived = models.BooleanField(default=False)
28
29 def get_tags(self):
30 """
31 Gets a sorted tag list.
32 """
33
34 return self.tags.order_by('name')
35
36 def bump(self):
37 """
38 Bumps (moves to up) thread if possible.
39 """
40
41 if self.can_bump():
42 self.bump_time = timezone.now()
43
44 logger.info('Bumped thread %d' % self.id)
45
46 def get_reply_count(self):
47 return self.replies.count()
48
49 def get_images_count(self):
50 # TODO Use sum
51 total_count = 0
52 for post_with_image in self.replies.annotate(images_count=Count(
53 'images')):
54 total_count += post_with_image.images_count
55 return total_count
56
57 def can_bump(self):
58 """
59 Checks if the thread can be bumped by replying to it.
60 """
61
62 if self.archived:
63 return False
64
65 post_count = self.get_reply_count()
66
67 return post_count < settings.MAX_POSTS_PER_THREAD
68
69 def delete_with_posts(self):
70 """
71 Completely deletes thread and all its posts
72 """
73
74 if self.replies.exists():
75 self.replies.all().delete()
76
77 self.delete()
78
79 def get_last_replies(self):
80 """
81 Gets several last replies, not including opening post
82 """
83
84 if settings.LAST_REPLIES_COUNT > 0:
85 reply_count = self.get_reply_count()
86
87 if reply_count > 0:
88 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
89 reply_count - 1)
90 replies = self.get_replies()
91 last_replies = replies[reply_count - reply_count_to_show:]
92
93 return last_replies
94
95 def get_skipped_replies_count(self):
96 """
97 Gets number of posts between opening post and last replies.
98 """
99 reply_count = self.get_reply_count()
100 last_replies_count = min(settings.LAST_REPLIES_COUNT,
101 reply_count - 1)
102 return reply_count - last_replies_count - 1
103
104 def get_replies(self, view_fields_only=False):
105 """
106 Gets sorted thread posts
107 """
108
109 query = self.replies.order_by('pub_time').prefetch_related('images')
110 if view_fields_only:
111 query = query.defer('poster_user_agent', 'text_markup_type')
112 return query.all()
113
114 def get_replies_with_images(self, view_fields_only=False):
115 return self.get_replies(view_fields_only).annotate(images_count=Count(
116 'images')).filter(images_count__gt=0)
117
118 def add_tag(self, tag):
119 """
120 Connects thread to a tag and tag to a thread
121 """
122
123 self.tags.add(tag)
124 tag.threads.add(self)
125
126 def remove_tag(self, tag):
127 self.tags.remove(tag)
128 tag.threads.remove(self)
129
130 def get_opening_post(self, only_id=False):
131 """
132 Gets the first post of the thread
133 """
134
135 query = self.replies.order_by('pub_time')
136 if only_id:
137 query = query.only('id')
138 opening_post = query.first()
139
140 return opening_post
141
142 def get_opening_post_id(self):
143 """
144 Gets ID of the first thread post.
145 """
146
147 cache_key = CACHE_KEY_OPENING_POST + str(self.id)
148 opening_post_id = cache.get(cache_key)
149 if not opening_post_id:
150 opening_post_id = self.get_opening_post(only_id=True).id
151 cache.set(cache_key, opening_post_id)
152
153 return opening_post_id
154
155 def __unicode__(self):
156 return str(self.id)
157
158 def get_pub_time(self):
159 """
160 Gets opening post's pub time because thread does not have its own one.
161 """
162
163 return self.get_opening_post().pub_time
@@ -0,0 +1,24 b''
1 from haystack import indexes
2 from boards.models import Post, Tag
3
4 __author__ = 'neko259'
5
6
7 class PostIndex(indexes.SearchIndex, indexes.Indexable):
8 text = indexes.CharField(document=True, use_template=True)
9
10 def get_model(self):
11 return Post
12
13 def index_queryset(self, using=None):
14 return self.get_model().objects.all()
15
16
17 class TagIndex(indexes.SearchIndex, indexes.Indexable):
18 text = indexes.CharField(document=True, use_template=True)
19
20 def get_model(self):
21 return Tag
22
23 def index_queryset(self, using=None):
24 return self.get_model().objects.get_not_empty_tags()
@@ -0,0 +1,1 b''
1 .hljs{display:block;padding:.5em;background:#f0f0f0}.hljs,.hljs-subst,.hljs-tag .hljs-title,.lisp .hljs-title,.clojure .hljs-built_in,.nginx .hljs-title{color:black}.hljs-string,.hljs-title,.hljs-constant,.hljs-parent,.hljs-tag .hljs-value,.hljs-rules .hljs-value,.hljs-rules .hljs-value .hljs-number,.hljs-preprocessor,.hljs-pragma,.haml .hljs-symbol,.ruby .hljs-symbol,.ruby .hljs-symbol .hljs-string,.hljs-aggregate,.hljs-template_tag,.django .hljs-variable,.smalltalk .hljs-class,.hljs-addition,.hljs-flow,.hljs-stream,.bash .hljs-variable,.apache .hljs-tag,.apache .hljs-cbracket,.tex .hljs-command,.tex .hljs-special,.erlang_repl .hljs-function_or_atom,.asciidoc .hljs-header,.markdown .hljs-header,.coffeescript .hljs-attribute{color:#800}.smartquote,.hljs-comment,.hljs-annotation,.hljs-template_comment,.diff .hljs-header,.hljs-chunk,.asciidoc .hljs-blockquote,.markdown .hljs-blockquote{color:#888}.hljs-number,.hljs-date,.hljs-regexp,.hljs-literal,.hljs-hexcolor,.smalltalk .hljs-symbol,.smalltalk .hljs-char,.go .hljs-constant,.hljs-change,.lasso .hljs-variable,.makefile .hljs-variable,.asciidoc .hljs-bullet,.markdown .hljs-bullet,.asciidoc .hljs-link_url,.markdown .hljs-link_url{color:#080}.hljs-label,.hljs-javadoc,.ruby .hljs-string,.hljs-decorator,.hljs-filter .hljs-argument,.hljs-localvars,.hljs-array,.hljs-attr_selector,.hljs-important,.hljs-pseudo,.hljs-pi,.haml .hljs-bullet,.hljs-doctype,.hljs-deletion,.hljs-envvar,.hljs-shebang,.apache .hljs-sqbracket,.nginx .hljs-built_in,.tex .hljs-formula,.erlang_repl .hljs-reserved,.hljs-prompt,.asciidoc .hljs-link_label,.markdown .hljs-link_label,.vhdl .hljs-attribute,.clojure .hljs-attribute,.asciidoc .hljs-attribute,.lasso .hljs-attribute,.coffeescript .hljs-property,.hljs-phony{color:#88F}.hljs-keyword,.hljs-id,.hljs-title,.hljs-built_in,.hljs-aggregate,.css .hljs-tag,.hljs-javadoctag,.hljs-phpdoc,.hljs-yardoctag,.smalltalk .hljs-class,.hljs-winutils,.bash .hljs-variable,.apache .hljs-tag,.go .hljs-typename,.tex .hljs-command,.asciidoc .hljs-strong,.markdown .hljs-strong,.hljs-request,.hljs-status{font-weight:bold}.asciidoc .hljs-emphasis,.markdown .hljs-emphasis{font-style:italic}.nginx .hljs-built_in{font-weight:normal}.coffeescript .javascript,.javascript .xml,.lasso .markup,.tex .hljs-formula,.xml .javascript,.xml .vbscript,.xml .css,.xml .hljs-cdata{opacity:.5} No newline at end of file
@@ -0,0 +1,1 b''
1 var hljs=new function(){function k(v){return v.replace(/&/gm,"&amp;").replace(/</gm,"&lt;").replace(/>/gm,"&gt;")}function t(v){return v.nodeName.toLowerCase()}function i(w,x){var v=w&&w.exec(x);return v&&v.index==0}function d(v){return Array.prototype.map.call(v.childNodes,function(w){if(w.nodeType==3){return b.useBR?w.nodeValue.replace(/\n/g,""):w.nodeValue}if(t(w)=="br"){return"\n"}return d(w)}).join("")}function r(w){var v=(w.className+" "+(w.parentNode?w.parentNode.className:"")).split(/\s+/);v=v.map(function(x){return x.replace(/^language-/,"")});return v.filter(function(x){return j(x)||x=="no-highlight"})[0]}function o(x,y){var v={};for(var w in x){v[w]=x[w]}if(y){for(var w in y){v[w]=y[w]}}return v}function u(x){var v=[];(function w(y,z){for(var A=y.firstChild;A;A=A.nextSibling){if(A.nodeType==3){z+=A.nodeValue.length}else{if(t(A)=="br"){z+=1}else{if(A.nodeType==1){v.push({event:"start",offset:z,node:A});z=w(A,z);v.push({event:"stop",offset:z,node:A})}}}}return z})(x,0);return v}function q(w,y,C){var x=0;var F="";var z=[];function B(){if(!w.length||!y.length){return w.length?w:y}if(w[0].offset!=y[0].offset){return(w[0].offset<y[0].offset)?w:y}return y[0].event=="start"?w:y}function A(H){function G(I){return" "+I.nodeName+'="'+k(I.value)+'"'}F+="<"+t(H)+Array.prototype.map.call(H.attributes,G).join("")+">"}function E(G){F+="</"+t(G)+">"}function v(G){(G.event=="start"?A:E)(G.node)}while(w.length||y.length){var D=B();F+=k(C.substr(x,D[0].offset-x));x=D[0].offset;if(D==w){z.reverse().forEach(E);do{v(D.splice(0,1)[0]);D=B()}while(D==w&&D.length&&D[0].offset==x);z.reverse().forEach(A)}else{if(D[0].event=="start"){z.push(D[0].node)}else{z.pop()}v(D.splice(0,1)[0])}}return F+k(C.substr(x))}function m(y){function v(z){return(z&&z.source)||z}function w(A,z){return RegExp(v(A),"m"+(y.cI?"i":"")+(z?"g":""))}function x(D,C){if(D.compiled){return}D.compiled=true;D.k=D.k||D.bK;if(D.k){var z={};function E(G,F){if(y.cI){F=F.toLowerCase()}F.split(" ").forEach(function(H){var I=H.split("|");z[I[0]]=[G,I[1]?Number(I[1]):1]})}if(typeof D.k=="string"){E("keyword",D.k)}else{Object.keys(D.k).forEach(function(F){E(F,D.k[F])})}D.k=z}D.lR=w(D.l||/\b[A-Za-z0-9_]+\b/,true);if(C){if(D.bK){D.b=D.bK.split(" ").join("|")}if(!D.b){D.b=/\B|\b/}D.bR=w(D.b);if(!D.e&&!D.eW){D.e=/\B|\b/}if(D.e){D.eR=w(D.e)}D.tE=v(D.e)||"";if(D.eW&&C.tE){D.tE+=(D.e?"|":"")+C.tE}}if(D.i){D.iR=w(D.i)}if(D.r===undefined){D.r=1}if(!D.c){D.c=[]}var B=[];D.c.forEach(function(F){if(F.v){F.v.forEach(function(G){B.push(o(F,G))})}else{B.push(F=="self"?D:F)}});D.c=B;D.c.forEach(function(F){x(F,D)});if(D.starts){x(D.starts,C)}var A=D.c.map(function(F){return F.bK?"\\.?\\b("+F.b+")\\b\\.?":F.b}).concat([D.tE]).concat([D.i]).map(v).filter(Boolean);D.t=A.length?w(A.join("|"),true):{exec:function(F){return null}};D.continuation={}}x(y)}function c(S,L,J,R){function v(U,V){for(var T=0;T<V.c.length;T++){if(i(V.c[T].bR,U)){return V.c[T]}}}function z(U,T){if(i(U.eR,T)){return U}if(U.eW){return z(U.parent,T)}}function A(T,U){return !J&&i(U.iR,T)}function E(V,T){var U=M.cI?T[0].toLowerCase():T[0];return V.k.hasOwnProperty(U)&&V.k[U]}function w(Z,X,W,V){var T=V?"":b.classPrefix,U='<span class="'+T,Y=W?"":"</span>";U+=Z+'">';return U+X+Y}function N(){var U=k(C);if(!I.k){return U}var T="";var X=0;I.lR.lastIndex=0;var V=I.lR.exec(U);while(V){T+=U.substr(X,V.index-X);var W=E(I,V);if(W){H+=W[1];T+=w(W[0],V[0])}else{T+=V[0]}X=I.lR.lastIndex;V=I.lR.exec(U)}return T+U.substr(X)}function F(){if(I.sL&&!f[I.sL]){return k(C)}var T=I.sL?c(I.sL,C,true,I.continuation.top):g(C);if(I.r>0){H+=T.r}if(I.subLanguageMode=="continuous"){I.continuation.top=T.top}return w(T.language,T.value,false,true)}function Q(){return I.sL!==undefined?F():N()}function P(V,U){var T=V.cN?w(V.cN,"",true):"";if(V.rB){D+=T;C=""}else{if(V.eB){D+=k(U)+T;C=""}else{D+=T;C=U}}I=Object.create(V,{parent:{value:I}})}function G(T,X){C+=T;if(X===undefined){D+=Q();return 0}var V=v(X,I);if(V){D+=Q();P(V,X);return V.rB?0:X.length}var W=z(I,X);if(W){var U=I;if(!(U.rE||U.eE)){C+=X}D+=Q();do{if(I.cN){D+="</span>"}H+=I.r;I=I.parent}while(I!=W.parent);if(U.eE){D+=k(X)}C="";if(W.starts){P(W.starts,"")}return U.rE?0:X.length}if(A(X,I)){throw new Error('Illegal lexeme "'+X+'" for mode "'+(I.cN||"<unnamed>")+'"')}C+=X;return X.length||1}var M=j(S);if(!M){throw new Error('Unknown language: "'+S+'"')}m(M);var I=R||M;var D="";for(var K=I;K!=M;K=K.parent){if(K.cN){D=w(K.cN,D,true)}}var C="";var H=0;try{var B,y,x=0;while(true){I.t.lastIndex=x;B=I.t.exec(L);if(!B){break}y=G(L.substr(x,B.index-x),B[0]);x=B.index+y}G(L.substr(x));for(var K=I;K.parent;K=K.parent){if(K.cN){D+="</span>"}}return{r:H,value:D,language:S,top:I}}catch(O){if(O.message.indexOf("Illegal")!=-1){return{r:0,value:k(L)}}else{throw O}}}function g(y,x){x=x||b.languages||Object.keys(f);var v={r:0,value:k(y)};var w=v;x.forEach(function(z){if(!j(z)){return}var A=c(z,y,false);A.language=z;if(A.r>w.r){w=A}if(A.r>v.r){w=v;v=A}});if(w.language){v.second_best=w}return v}function h(v){if(b.tabReplace){v=v.replace(/^((<[^>]+>|\t)+)/gm,function(w,z,y,x){return z.replace(/\t/g,b.tabReplace)})}if(b.useBR){v=v.replace(/\n/g,"<br>")}return v}function p(z){var y=d(z);var A=r(z);if(A=="no-highlight"){return}var v=A?c(A,y,true):g(y);var w=u(z);if(w.length){var x=document.createElementNS("http://www.w3.org/1999/xhtml","pre");x.innerHTML=v.value;v.value=q(w,u(x),y)}v.value=h(v.value);z.innerHTML=v.value;z.className+=" hljs "+(!A&&v.language||"");z.result={language:v.language,re:v.r};if(v.second_best){z.second_best={language:v.second_best.language,re:v.second_best.r}}}var b={classPrefix:"hljs-",tabReplace:null,useBR:false,languages:undefined};function s(v){b=o(b,v)}function l(){if(l.called){return}l.called=true;var v=document.querySelectorAll("pre code");Array.prototype.forEach.call(v,p)}function a(){addEventListener("DOMContentLoaded",l,false);addEventListener("load",l,false)}var f={};var n={};function e(v,x){var w=f[v]=x(this);if(w.aliases){w.aliases.forEach(function(y){n[y]=v})}}function j(v){return f[v]||f[n[v]]}this.highlight=c;this.highlightAuto=g;this.fixMarkup=h;this.highlightBlock=p;this.configure=s;this.initHighlighting=l;this.initHighlightingOnLoad=a;this.registerLanguage=e;this.getLanguage=j;this.inherit=o;this.IR="[a-zA-Z][a-zA-Z0-9_]*";this.UIR="[a-zA-Z_][a-zA-Z0-9_]*";this.NR="\\b\\d+(\\.\\d+)?";this.CNR="(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)";this.BNR="\\b(0b[01]+)";this.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~";this.BE={b:"\\\\[\\s\\S]",r:0};this.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[this.BE]};this.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[this.BE]};this.CLCM={cN:"comment",b:"//",e:"$"};this.CBLCLM={cN:"comment",b:"/\\*",e:"\\*/"};this.HCM={cN:"comment",b:"#",e:"$"};this.NM={cN:"number",b:this.NR,r:0};this.CNM={cN:"number",b:this.CNR,r:0};this.BNM={cN:"number",b:this.BNR,r:0};this.REGEXP_MODE={cN:"regexp",b:/\//,e:/\/[gim]*/,i:/\n/,c:[this.BE,{b:/\[/,e:/\]/,r:0,c:[this.BE]}]};this.TM={cN:"title",b:this.IR,r:0};this.UTM={cN:"title",b:this.UIR,r:0}}();hljs.registerLanguage("bash",function(b){var a={cN:"variable",v:[{b:/\$[\w\d#@][\w\d_]*/},{b:/\$\{(.*?)\}/}]};var d={cN:"string",b:/"/,e:/"/,c:[b.BE,a,{cN:"variable",b:/\$\(/,e:/\)/,c:[b.BE]}]};var c={cN:"string",b:/'/,e:/'/};return{l:/-?[a-z\.]+/,k:{keyword:"if then else elif fi for break continue while in do done exit return set declare case esac export exec",literal:"true false",built_in:"printf echo read cd pwd pushd popd dirs let eval unset typeset readonly getopts source shopt caller type hash bind help sudo",operator:"-ne -eq -lt -gt -f -d -e -s -l -a"},c:[{cN:"shebang",b:/^#![^\n]+sh\s*$/,r:10},{cN:"function",b:/\w[\w\d_]*\s*\(\s*\)\s*\{/,rB:true,c:[b.inherit(b.TM,{b:/\w[\w\d_]*/})],r:0},b.HCM,b.NM,d,c,a]}});hljs.registerLanguage("cs",function(b){var a="abstract as base bool break byte case catch char checked const continue decimal default delegate do double else enum event explicit extern false finally fixed float for foreach goto if implicit in int interface internal is lock long new null object operator out override params private protected public readonly ref return sbyte sealed short sizeof stackalloc static string struct switch this throw true try typeof uint ulong unchecked unsafe ushort using virtual volatile void while async await ascending descending from get group into join let orderby partial select set value var where yield";return{k:a,c:[{cN:"comment",b:"///",e:"$",rB:true,c:[{cN:"xmlDocTag",b:"///|<!--|-->"},{cN:"xmlDocTag",b:"</?",e:">"}]},b.CLCM,b.CBLCLM,{cN:"preprocessor",b:"#",e:"$",k:"if else elif endif define undef warning error line region endregion pragma checksum"},{cN:"string",b:'@"',e:'"',c:[{b:'""'}]},b.ASM,b.QSM,b.CNM,{bK:"protected public private internal",e:/[{;=]/,k:a,c:[{bK:"class namespace interface",starts:{c:[b.TM]}},{b:b.IR+"\\s*\\(",rB:true,c:[b.TM]}]}]}});hljs.registerLanguage("ruby",function(e){var h="[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?";var g="and false then defined module in return redo if BEGIN retry end for true self when next until do begin unless END rescue nil else break undef not super class case require yield alias while ensure elsif or include attr_reader attr_writer attr_accessor";var a={cN:"yardoctag",b:"@[A-Za-z]+"};var i={cN:"comment",v:[{b:"#",e:"$",c:[a]},{b:"^\\=begin",e:"^\\=end",c:[a],r:10},{b:"^__END__",e:"\\n$"}]};var c={cN:"subst",b:"#\\{",e:"}",k:g};var d={cN:"string",c:[e.BE,c],v:[{b:/'/,e:/'/},{b:/"/,e:/"/},{b:"%[qw]?\\(",e:"\\)"},{b:"%[qw]?\\[",e:"\\]"},{b:"%[qw]?{",e:"}"},{b:"%[qw]?<",e:">",r:10},{b:"%[qw]?/",e:"/",r:10},{b:"%[qw]?%",e:"%",r:10},{b:"%[qw]?-",e:"-",r:10},{b:"%[qw]?\\|",e:"\\|",r:10},{b:/\B\?(\\\d{1,3}|\\x[A-Fa-f0-9]{1,2}|\\u[A-Fa-f0-9]{4}|\\?\S)\b/}]};var b={cN:"params",b:"\\(",e:"\\)",k:g};var f=[d,i,{cN:"class",bK:"class module",e:"$|;",i:/=/,c:[e.inherit(e.TM,{b:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?"}),{cN:"inheritance",b:"<\\s*",c:[{cN:"parent",b:"("+e.IR+"::)?"+e.IR}]},i]},{cN:"function",bK:"def",e:" |$|;",r:0,c:[e.inherit(e.TM,{b:h}),b,i]},{cN:"constant",b:"(::)?(\\b[A-Z]\\w*(::)?)+",r:0},{cN:"symbol",b:":",c:[d,{b:h}],r:0},{cN:"symbol",b:e.UIR+"(\\!|\\?)?:",r:0},{cN:"number",b:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",r:0},{cN:"variable",b:"(\\$\\W)|((\\$|\\@\\@?)(\\w+))"},{b:"("+e.RSR+")\\s*",c:[i,{cN:"regexp",c:[e.BE,c],i:/\n/,v:[{b:"/",e:"/[a-z]*"},{b:"%r{",e:"}[a-z]*"},{b:"%r\\(",e:"\\)[a-z]*"},{b:"%r!",e:"![a-z]*"},{b:"%r\\[",e:"\\][a-z]*"}]}],r:0}];c.c=f;b.c=f;return{k:g,c:f}});hljs.registerLanguage("diff",function(a){return{c:[{cN:"chunk",r:10,v:[{b:/^\@\@ +\-\d+,\d+ +\+\d+,\d+ +\@\@$/},{b:/^\*\*\* +\d+,\d+ +\*\*\*\*$/},{b:/^\-\-\- +\d+,\d+ +\-\-\-\-$/}]},{cN:"header",v:[{b:/Index: /,e:/$/},{b:/=====/,e:/=====$/},{b:/^\-\-\-/,e:/$/},{b:/^\*{3} /,e:/$/},{b:/^\+\+\+/,e:/$/},{b:/\*{5}/,e:/\*{5}$/}]},{cN:"addition",b:"^\\+",e:"$"},{cN:"deletion",b:"^\\-",e:"$"},{cN:"change",b:"^\\!",e:"$"}]}});hljs.registerLanguage("javascript",function(a){return{aliases:["js"],k:{keyword:"in if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const class",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require"},c:[{cN:"pi",b:/^\s*('|")use strict('|")/,r:10},a.ASM,a.QSM,a.CLCM,a.CBLCLM,a.CNM,{b:"("+a.RSR+"|\\b(case|return|throw)\\b)\\s*",k:"return throw case",c:[a.CLCM,a.CBLCLM,a.REGEXP_MODE,{b:/</,e:/>;/,r:0,sL:"xml"}],r:0},{cN:"function",bK:"function",e:/\{/,c:[a.inherit(a.TM,{b:/[A-Za-z$_][0-9A-Za-z$_]*/}),{cN:"params",b:/\(/,e:/\)/,c:[a.CLCM,a.CBLCLM],i:/["'\(]/}],i:/\[|%/},{b:/\$[(.]/},{b:"\\."+a.IR,r:0}]}});hljs.registerLanguage("xml",function(a){var c="[A-Za-z0-9\\._:-]+";var d={b:/<\?(php)?(?!\w)/,e:/\?>/,sL:"php",subLanguageMode:"continuous"};var b={eW:true,i:/</,r:0,c:[d,{cN:"attribute",b:c,r:0},{b:"=",r:0,c:[{cN:"value",v:[{b:/"/,e:/"/},{b:/'/,e:/'/},{b:/[^\s\/>]+/}]}]}]};return{aliases:["html"],cI:true,c:[{cN:"doctype",b:"<!DOCTYPE",e:">",r:10,c:[{b:"\\[",e:"\\]"}]},{cN:"comment",b:"<!--",e:"-->",r:10},{cN:"cdata",b:"<\\!\\[CDATA\\[",e:"\\]\\]>",r:10},{cN:"tag",b:"<style(?=\\s|>|$)",e:">",k:{title:"style"},c:[b],starts:{e:"</style>",rE:true,sL:"css"}},{cN:"tag",b:"<script(?=\\s|>|$)",e:">",k:{title:"script"},c:[b],starts:{e:"<\/script>",rE:true,sL:"javascript"}},{b:"<%",e:"%>",sL:"vbscript"},d,{cN:"pi",b:/<\?\w+/,e:/\?>/,r:10},{cN:"tag",b:"</?",e:"/?>",c:[{cN:"title",b:"[^ /><]+",r:0},b]}]}});hljs.registerLanguage("markdown",function(a){return{c:[{cN:"header",v:[{b:"^#{1,6}",e:"$"},{b:"^.+?\\n[=-]{2,}$"}]},{b:"<",e:">",sL:"xml",r:0},{cN:"bullet",b:"^([*+-]|(\\d+\\.))\\s+"},{cN:"strong",b:"[*_]{2}.+?[*_]{2}"},{cN:"emphasis",v:[{b:"\\*.+?\\*"},{b:"_.+?_",r:0}]},{cN:"blockquote",b:"^>\\s+",e:"$"},{cN:"code",v:[{b:"`.+?`"},{b:"^( {4}|\t)",e:"$",r:0}]},{cN:"horizontal_rule",b:"^[-\\*]{3,}",e:"$"},{b:"\\[.+?\\][\\(\\[].+?[\\)\\]]",rB:true,c:[{cN:"link_label",b:"\\[",e:"\\]",eB:true,rE:true,r:0},{cN:"link_url",b:"\\]\\(",e:"\\)",eB:true,eE:true},{cN:"link_reference",b:"\\]\\[",e:"\\]",eB:true,eE:true,}],r:10},{b:"^\\[.+\\]:",e:"$",rB:true,c:[{cN:"link_reference",b:"\\[",e:"\\]",eB:true,eE:true},{cN:"link_url",b:"\\s",e:"$"}]}]}});hljs.registerLanguage("css",function(a){var b="[a-zA-Z-][a-zA-Z0-9_-]*";var c={cN:"function",b:b+"\\(",e:"\\)",c:["self",a.NM,a.ASM,a.QSM]};return{cI:true,i:"[=/|']",c:[a.CBLCLM,{cN:"id",b:"\\#[A-Za-z0-9_-]+"},{cN:"class",b:"\\.[A-Za-z0-9_-]+",r:0},{cN:"attr_selector",b:"\\[",e:"\\]",i:"$"},{cN:"pseudo",b:":(:)?[a-zA-Z0-9\\_\\-\\+\\(\\)\\\"\\']+"},{cN:"at_rule",b:"@(font-face|page)",l:"[a-z-]+",k:"font-face page"},{cN:"at_rule",b:"@",e:"[{;]",c:[{cN:"keyword",b:/\S+/},{b:/\s/,eW:true,eE:true,r:0,c:[c,a.ASM,a.QSM,a.NM]}]},{cN:"tag",b:b,r:0},{cN:"rules",b:"{",e:"}",i:"[^\\s]",r:0,c:[a.CBLCLM,{cN:"rule",b:"[^\\s]",rB:true,e:";",eW:true,c:[{cN:"attribute",b:"[A-Z\\_\\.\\-]+",e:":",eE:true,i:"[^\\s]",starts:{cN:"value",eW:true,eE:true,c:[c,a.NM,a.QSM,a.ASM,a.CBLCLM,{cN:"hexcolor",b:"#[0-9A-Fa-f]+"},{cN:"important",b:"!important"}]}}]}]}]}});hljs.registerLanguage("http",function(a){return{i:"\\S",c:[{cN:"status",b:"^HTTP/[0-9\\.]+",e:"$",c:[{cN:"number",b:"\\b\\d{3}\\b"}]},{cN:"request",b:"^[A-Z]+ (.*?) HTTP/[0-9\\.]+$",rB:true,e:"$",c:[{cN:"string",b:" ",e:" ",eB:true,eE:true}]},{cN:"attribute",b:"^\\w",e:": ",eE:true,i:"\\n|\\s|=",starts:{cN:"string",e:"$"}},{b:"\\n\\n",starts:{sL:"",eW:true}}]}});hljs.registerLanguage("java",function(b){var a="false synchronized int abstract float private char boolean static null if const for true while long throw strictfp finally protected import native final return void enum else break transient new catch instanceof byte super volatile case assert short package default double public try this switch continue throws";return{k:a,i:/<\//,c:[{cN:"javadoc",b:"/\\*\\*",e:"\\*/",c:[{cN:"javadoctag",b:"(^|\\s)@[A-Za-z]+"}],r:10},b.CLCM,b.CBLCLM,b.ASM,b.QSM,{bK:"protected public private",e:/[{;=]/,k:a,c:[{cN:"class",bK:"class interface",eW:true,i:/[:"<>]/,c:[{bK:"extends implements",r:10},b.UTM]},{b:b.UIR+"\\s*\\(",rB:true,c:[b.UTM]}]},b.CNM,{cN:"annotation",b:"@[A-Za-z]+"}]}});hljs.registerLanguage("php",function(b){var e={cN:"variable",b:"\\$+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*"};var a={cN:"preprocessor",b:/<\?(php)?|\?>/};var c={cN:"string",c:[b.BE,a],v:[{b:'b"',e:'"'},{b:"b'",e:"'"},b.inherit(b.ASM,{i:null}),b.inherit(b.QSM,{i:null})]};var d={v:[b.BNM,b.CNM]};return{cI:true,k:"and include_once list abstract global private echo interface as static endswitch array null if endwhile or const for endforeach self var while isset public protected exit foreach throw elseif include __FILE__ empty require_once do xor return parent clone use __CLASS__ __LINE__ else break print eval new catch __METHOD__ case exception default die require __FUNCTION__ enddeclare final try switch continue endfor endif declare unset true false trait goto instanceof insteadof __DIR__ __NAMESPACE__ yield finally",c:[b.CLCM,b.HCM,{cN:"comment",b:"/\\*",e:"\\*/",c:[{cN:"phpdoc",b:"\\s@[A-Za-z]+"},a]},{cN:"comment",b:"__halt_compiler.+?;",eW:true,k:"__halt_compiler",l:b.UIR},{cN:"string",b:"<<<['\"]?\\w+['\"]?$",e:"^\\w+;",c:[b.BE]},a,e,{cN:"function",bK:"function",e:/[;{]/,i:"\\$|\\[|%",c:[b.UTM,{cN:"params",b:"\\(",e:"\\)",c:["self",e,b.CBLCLM,c,d]}]},{cN:"class",bK:"class interface",e:"{",i:/[:\(\$"]/,c:[{bK:"extends implements",r:10},b.UTM]},{bK:"namespace",e:";",i:/[\.']/,c:[b.UTM]},{bK:"use",e:";",c:[b.UTM]},{b:"=>"},c,d]}});hljs.registerLanguage("python",function(a){var f={cN:"prompt",b:/^(>>>|\.\.\.) /};var b={cN:"string",c:[a.BE],v:[{b:/(u|b)?r?'''/,e:/'''/,c:[f],r:10},{b:/(u|b)?r?"""/,e:/"""/,c:[f],r:10},{b:/(u|r|ur)'/,e:/'/,r:10},{b:/(u|r|ur)"/,e:/"/,r:10},{b:/(b|br)'/,e:/'/,},{b:/(b|br)"/,e:/"/,},a.ASM,a.QSM]};var d={cN:"number",r:0,v:[{b:a.BNR+"[lLjJ]?"},{b:"\\b(0o[0-7]+)[lLjJ]?"},{b:a.CNR+"[lLjJ]?"}]};var e={cN:"params",b:/\(/,e:/\)/,c:["self",f,d,b]};var c={e:/:/,i:/[${=;\n]/,c:[a.UTM,e]};return{k:{keyword:"and elif is global as in if from raise for except finally print import pass return exec else break not with class assert yield try while continue del or def lambda nonlocal|10 None True False",built_in:"Ellipsis NotImplemented"},i:/(<\/|->|\?)/,c:[f,d,b,a.HCM,a.inherit(c,{cN:"function",bK:"def",r:10}),a.inherit(c,{cN:"class",bK:"class"}),{cN:"decorator",b:/@/,e:/$/},{b:/\b(print|exec)\(/}]}});hljs.registerLanguage("sql",function(a){return{cI:true,i:/[<>]/,c:[{cN:"operator",b:"\\b(begin|end|start|commit|rollback|savepoint|lock|alter|create|drop|rename|call|delete|do|handler|insert|load|replace|select|truncate|update|set|show|pragma|grant|merge)\\b(?!:)",e:";",eW:true,k:{keyword:"all partial global month current_timestamp using go revoke smallint indicator end-exec disconnect zone with character assertion to add current_user usage input local alter match collate real then rollback get read timestamp session_user not integer bit unique day minute desc insert execute like ilike|2 level decimal drop continue isolation found where constraints domain right national some module transaction relative second connect escape close system_user for deferred section cast current sqlstate allocate intersect deallocate numeric public preserve full goto initially asc no key output collation group by union session both last language constraint column of space foreign deferrable prior connection unknown action commit view or first into float year primary cascaded except restrict set references names table outer open select size are rows from prepare distinct leading create only next inner authorization schema corresponding option declare precision immediate else timezone_minute external varying translation true case exception join hour default double scroll value cursor descriptor values dec fetch procedure delete and false int is describe char as at in varchar null trailing any absolute current_time end grant privileges when cross check write current_date pad begin temporary exec time update catalog user sql date on identity timezone_hour natural whenever interval work order cascade diagnostics nchar having left call do handler load replace truncate start lock show pragma exists number trigger if before after each row merge matched database",aggregate:"count sum min max avg"},c:[{cN:"string",b:"'",e:"'",c:[a.BE,{b:"''"}]},{cN:"string",b:'"',e:'"',c:[a.BE,{b:'""'}]},{cN:"string",b:"`",e:"`",c:[a.BE]},a.CNM]},a.CBLCLM,{cN:"comment",b:"--",e:"$"}]}});hljs.registerLanguage("ini",function(a){return{cI:true,i:/\S/,c:[{cN:"comment",b:";",e:"$"},{cN:"title",b:"^\\[",e:"\\]"},{cN:"setting",b:"^[a-z0-9\\[\\]_-]+[ \\t]*=[ \\t]*",e:"$",c:[{cN:"value",eW:true,k:"on off true false yes no",c:[a.QSM,a.NM],r:0}]}]}});hljs.registerLanguage("perl",function(c){var d="getpwent getservent quotemeta msgrcv scalar kill dbmclose undef lc ma syswrite tr send umask sysopen shmwrite vec qx utime local oct semctl localtime readpipe do return format read sprintf dbmopen pop getpgrp not getpwnam rewinddir qqfileno qw endprotoent wait sethostent bless s|0 opendir continue each sleep endgrent shutdown dump chomp connect getsockname die socketpair close flock exists index shmgetsub for endpwent redo lstat msgctl setpgrp abs exit select print ref gethostbyaddr unshift fcntl syscall goto getnetbyaddr join gmtime symlink semget splice x|0 getpeername recv log setsockopt cos last reverse gethostbyname getgrnam study formline endhostent times chop length gethostent getnetent pack getprotoent getservbyname rand mkdir pos chmod y|0 substr endnetent printf next open msgsnd readdir use unlink getsockopt getpriority rindex wantarray hex system getservbyport endservent int chr untie rmdir prototype tell listen fork shmread ucfirst setprotoent else sysseek link getgrgid shmctl waitpid unpack getnetbyname reset chdir grep split require caller lcfirst until warn while values shift telldir getpwuid my getprotobynumber delete and sort uc defined srand accept package seekdir getprotobyname semop our rename seek if q|0 chroot sysread setpwent no crypt getc chown sqrt write setnetent setpriority foreach tie sin msgget map stat getlogin unless elsif truncate exec keys glob tied closedirioctl socket readlink eval xor readline binmode setservent eof ord bind alarm pipe atan2 getgrent exp time push setgrent gt lt or ne m|0 break given say state when";var f={cN:"subst",b:"[$@]\\{",e:"\\}",k:d};var g={b:"->{",e:"}"};var a={cN:"variable",v:[{b:/\$\d/},{b:/[\$\%\@\*](\^\w\b|#\w+(\:\:\w+)*|{\w+}|\w+(\:\:\w*)*)/},{b:/[\$\%\@\*][^\s\w{]/,r:0}]};var e={cN:"comment",b:"^(__END__|__DATA__)",e:"\\n$",r:5};var h=[c.BE,f,a];var b=[a,c.HCM,e,{cN:"comment",b:"^\\=\\w",e:"\\=cut",eW:true},g,{cN:"string",c:h,v:[{b:"q[qwxr]?\\s*\\(",e:"\\)",r:5},{b:"q[qwxr]?\\s*\\[",e:"\\]",r:5},{b:"q[qwxr]?\\s*\\{",e:"\\}",r:5},{b:"q[qwxr]?\\s*\\|",e:"\\|",r:5},{b:"q[qwxr]?\\s*\\<",e:"\\>",r:5},{b:"qw\\s+q",e:"q",r:5},{b:"'",e:"'",c:[c.BE]},{b:'"',e:'"'},{b:"`",e:"`",c:[c.BE]},{b:"{\\w+}",c:[],r:0},{b:"-?\\w+\\s*\\=\\>",c:[],r:0}]},{cN:"number",b:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",r:0},{b:"(\\/\\/|"+c.RSR+"|\\b(split|return|print|reverse|grep)\\b)\\s*",k:"split return print reverse grep",r:0,c:[c.HCM,e,{cN:"regexp",b:"(s|tr|y)/(\\\\.|[^/])*/(\\\\.|[^/])*/[a-z]*",r:10},{cN:"regexp",b:"(m|qr)?/",e:"/[a-z]*",c:[c.BE],r:0}]},{cN:"sub",bK:"sub",e:"(\\s*\\(.*?\\))?[;{]",r:5},{cN:"operator",b:"-\\w\\b",r:0}];f.c=b;g.c=b;return{k:d,c:b}});hljs.registerLanguage("objectivec",function(a){var d={keyword:"int float while char export sizeof typedef const struct for union unsigned long volatile static bool mutable if do return goto void enum else break extern asm case short default double register explicit signed typename this switch continue wchar_t inline readonly assign self synchronized id nonatomic super unichar IBOutlet IBAction strong weak @private @protected @public @try @property @end @throw @catch @finally @synthesize @dynamic @selector @optional @required",literal:"false true FALSE TRUE nil YES NO NULL",built_in:"NSString NSDictionary CGRect CGPoint UIButton UILabel UITextView UIWebView MKMapView UISegmentedControl NSObject UITableViewDelegate UITableViewDataSource NSThread UIActivityIndicator UITabbar UIToolBar UIBarButtonItem UIImageView NSAutoreleasePool UITableView BOOL NSInteger CGFloat NSException NSLog NSMutableString NSMutableArray NSMutableDictionary NSURL NSIndexPath CGSize UITableViewCell UIView UIViewController UINavigationBar UINavigationController UITabBarController UIPopoverController UIPopoverControllerDelegate UIImage NSNumber UISearchBar NSFetchedResultsController NSFetchedResultsChangeType UIScrollView UIScrollViewDelegate UIEdgeInsets UIColor UIFont UIApplication NSNotFound NSNotificationCenter NSNotification UILocalNotification NSBundle NSFileManager NSTimeInterval NSDate NSCalendar NSUserDefaults UIWindow NSRange NSArray NSError NSURLRequest NSURLConnection UIInterfaceOrientation MPMoviePlayerController dispatch_once_t dispatch_queue_t dispatch_sync dispatch_async dispatch_once"};var c=/[a-zA-Z@][a-zA-Z0-9_]*/;var b="@interface @class @protocol @implementation";return{k:d,l:c,i:"</",c:[a.CLCM,a.CBLCLM,a.CNM,a.QSM,{cN:"string",b:"'",e:"[^\\\\]'",i:"[^\\\\][^']"},{cN:"preprocessor",b:"#import",e:"$",c:[{cN:"title",b:'"',e:'"'},{cN:"title",b:"<",e:">"}]},{cN:"preprocessor",b:"#",e:"$"},{cN:"class",b:"("+b.split(" ").join("|")+")\\b",e:"({|$)",k:b,l:c,c:[a.UTM]},{cN:"variable",b:"\\."+a.UIR,r:0}]}});hljs.registerLanguage("coffeescript",function(c){var b={keyword:"in if for while finally new do return else break catch instanceof throw try this switch continue typeof delete debugger super then unless until loop of by when and or is isnt not",literal:"true false null undefined yes no on off",reserved:"case default function var void with const let enum export import native __hasProp __extends __slice __bind __indexOf",built_in:"npm require console print module exports global window document"};var a="[A-Za-z$_][0-9A-Za-z$_]*";var f=c.inherit(c.TM,{b:a});var e={cN:"subst",b:/#\{/,e:/}/,k:b};var d=[c.BNM,c.inherit(c.CNM,{starts:{e:"(\\s*/)?",r:0}}),{cN:"string",v:[{b:/'''/,e:/'''/,c:[c.BE]},{b:/'/,e:/'/,c:[c.BE]},{b:/"""/,e:/"""/,c:[c.BE,e]},{b:/"/,e:/"/,c:[c.BE,e]}]},{cN:"regexp",v:[{b:"///",e:"///",c:[e,c.HCM]},{b:"//[gim]*",r:0},{b:"/\\S(\\\\.|[^\\n])*?/[gim]*(?=\\s|\\W|$)"}]},{cN:"property",b:"@"+a},{b:"`",e:"`",eB:true,eE:true,sL:"javascript"}];e.c=d;return{k:b,c:d.concat([{cN:"comment",b:"###",e:"###"},c.HCM,{cN:"function",b:"("+a+"\\s*=\\s*)?(\\(.*\\))?\\s*\\B[-=]>",e:"[-=]>",rB:true,c:[f,{cN:"params",b:"\\(",rB:true,c:[{b:/\(/,e:/\)/,k:b,c:["self"].concat(d)}]}]},{cN:"class",bK:"class",e:"$",i:/[:="\[\]]/,c:[{bK:"extends",eW:true,i:/[:="\[\]]/,c:[f]},f]},{cN:"attribute",b:a+":",e:":",rB:true,eE:true,r:0}])}});hljs.registerLanguage("nginx",function(c){var b={cN:"variable",v:[{b:/\$\d+/},{b:/\$\{/,e:/}/},{b:"[\\$\\@]"+c.UIR}]};var a={eW:true,l:"[a-z/_]+",k:{built_in:"on off yes no true false none blocked debug info notice warn error crit select break last permanent redirect kqueue rtsig epoll poll /dev/poll"},r:0,i:"=>",c:[c.HCM,{cN:"string",c:[c.BE,b],v:[{b:/"/,e:/"/},{b:/'/,e:/'/}]},{cN:"url",b:"([a-z]+):/",e:"\\s",eW:true,eE:true},{cN:"regexp",c:[c.BE,b],v:[{b:"\\s\\^",e:"\\s|{|;",rE:true},{b:"~\\*?\\s+",e:"\\s|{|;",rE:true},{b:"\\*(\\.[a-z\\-]+)+"},{b:"([a-z\\-]+\\.)+\\*"}]},{cN:"number",b:"\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}(:\\d{1,5})?\\b"},{cN:"number",b:"\\b\\d+[kKmMgGdshdwy]*\\b",r:0},b]};return{c:[c.HCM,{b:c.UIR+"\\s",e:";|{",rB:true,c:[c.inherit(c.UTM,{starts:a})],r:0}],i:"[^\\s\\}]"}});hljs.registerLanguage("json",function(a){var e={literal:"true false null"};var d=[a.QSM,a.CNM];var c={cN:"value",e:",",eW:true,eE:true,c:d,k:e};var b={b:"{",e:"}",c:[{cN:"attribute",b:'\\s*"',e:'"\\s*:\\s*',eB:true,eE:true,c:[a.BE],i:"\\n",starts:c}],i:"\\S"};var f={b:"\\[",e:"\\]",c:[a.inherit(c,{cN:null})],i:"\\S"};d.splice(d.length,0,b,f);return{c:d,k:e,i:"\\S"}});hljs.registerLanguage("apache",function(a){var b={cN:"number",b:"[\\$%]\\d+"};return{cI:true,c:[a.HCM,{cN:"tag",b:"</?",e:">"},{cN:"keyword",b:/\w+/,r:0,k:{common:"order deny allow setenv rewriterule rewriteengine rewritecond documentroot sethandler errordocument loadmodule options header listen serverroot servername"},starts:{e:/$/,r:0,k:{literal:"on off all"},c:[{cN:"sqbracket",b:"\\s\\[",e:"\\]$"},{cN:"cbracket",b:"[\\$%]\\{",e:"\\}",c:["self",b]},b,a.QSM]}}],i:/\S/}});hljs.registerLanguage("cpp",function(a){var b={keyword:"false int float while private char catch export virtual operator sizeof dynamic_cast|10 typedef const_cast|10 const struct for static_cast|10 union namespace unsigned long throw volatile static protected bool template mutable if public friend do return goto auto void enum else break new extern using true class asm case typeid short reinterpret_cast|10 default double register explicit signed typename try this switch continue wchar_t inline delete alignof char16_t char32_t constexpr decltype noexcept nullptr static_assert thread_local restrict _Bool complex _Complex _Imaginary",built_in:"std string cin cout cerr clog stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap array shared_ptr abort abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf"};return{aliases:["c"],k:b,i:"</",c:[a.CLCM,a.CBLCLM,a.QSM,{cN:"string",b:"'\\\\?.",e:"'",i:"."},{cN:"number",b:"\\b(\\d+(\\.\\d*)?|\\.\\d+)(u|U|l|L|ul|UL|f|F)"},a.CNM,{cN:"preprocessor",b:"#",e:"$",c:[{b:"include\\s*<",e:">",i:"\\n"},a.CLCM]},{cN:"stl_container",b:"\\b(deque|list|queue|stack|vector|map|set|bitset|multiset|multimap|unordered_map|unordered_set|unordered_multiset|unordered_multimap|array)\\s*<",e:">",k:b,r:10,c:["self"]}]}});hljs.registerLanguage("makefile",function(a){var b={cN:"variable",b:/\$\(/,e:/\)/,c:[a.BE]};return{c:[a.HCM,{b:/^\w+\s*\W*=/,rB:true,r:0,starts:{cN:"constant",e:/\s*\W*=/,eE:true,starts:{e:/$/,r:0,c:[b],}}},{cN:"title",b:/^[\w]+:\s*$/},{cN:"phony",b:/^\.PHONY:/,e:/$/,k:".PHONY",l:/[\.\w]+/},{b:/^\t+/,e:/$/,c:[a.QSM,b]}]}}); No newline at end of file
@@ -0,0 +1,2 b''
1 {{ object.title }}
2 {{ object.text }} No newline at end of file
@@ -0,0 +1,1 b''
1 {{ object.name }} No newline at end of file
@@ -0,0 +1,29 b''
1 {% extends 'boards/base.html' %}
2
3 {% load board %}
4 {% load i18n %}
5
6 {% block content %}
7 <div class="post-form-w">
8 <h3>{% trans 'Search' %}</h3>
9 <form method="get" action=".">
10 {{ form.as_p }}
11 <input type="submit" value="{% trans 'Search' %}">
12 </form>
13 </div>
14
15 {% if query %}
16 {% for result in page.object_list %}
17 {{ result.object.get_view }}
18 {% empty %}
19 <div class="post">{% trans 'No results found.' %}</div>
20 {% endfor %}
21
22 {% if page.has_previous or page.has_next %}
23 <div>
24 {% if page.has_previous %}<a href="?q={{ query }}&amp;page={{ page.previous_page_number }}">{% endif %}&laquo; {% trans 'Previous' %}{% if page.has_previous %}</a>{% endif %}
25 {% if page.has_next %}<a href="?q={{ query }}&amp;page= {{ page.next_page_number }}">{% endif %}{% trans 'Next' %} &raquo; {% if page.has_next %}</a>{% endif %}
26 </div>
27 {% endif %}
28 {% endif %}
29 {% endblock %} No newline at end of file
@@ -1,351 +1,351 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, Post
12 from boards.models import User, Post, 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. You can reply to message >>123 like
24 this. 2 new lines are required to start new paragraph.''')
24 this. 2 new lines are required to start new paragraph.''')
25 TAGS_PLACEHOLDER = _('tag1 several_words_tag')
25 TAGS_PLACEHOLDER = _('tag1 several_words_tag')
26
26
27 ERROR_IMAGE_DUPLICATE = _('Such image was already posted')
27 ERROR_IMAGE_DUPLICATE = _('Such image was already posted')
28
28
29 LABEL_TITLE = _('Title')
29 LABEL_TITLE = _('Title')
30 LABEL_TEXT = _('Text')
30 LABEL_TEXT = _('Text')
31 LABEL_TAG = _('Tag')
31 LABEL_TAG = _('Tag')
32
32
33 TAG_MAX_LENGTH = 20
33 TAG_MAX_LENGTH = 20
34
34
35 REGEX_TAG = ur'^[\w\d]+$'
35 REGEX_TAG = ur'^[\w\d]+$'
36
36
37
37
38 class FormatPanel(forms.Textarea):
38 class FormatPanel(forms.Textarea):
39 def render(self, name, value, attrs=None):
39 def render(self, name, value, attrs=None):
40 output = '<div id="mark-panel">'
40 output = '<div id="mark-panel">'
41 for formatter in formatters:
41 for formatter in formatters:
42 output += u'<span class="mark_btn"' + \
42 output += u'<span class="mark_btn"' + \
43 u' onClick="addMarkToMsg(\'' + formatter.format_left + \
43 u' onClick="addMarkToMsg(\'' + formatter.format_left + \
44 '\', \'' + formatter.format_right + '\')">' + \
44 '\', \'' + formatter.format_right + '\')">' + \
45 formatter.preview_left + formatter.name + \
45 formatter.preview_left + formatter.name + \
46 formatter.preview_right + u'</span>'
46 formatter.preview_right + u'</span>'
47
47
48 output += '</div>'
48 output += '</div>'
49 output += super(FormatPanel, self).render(name, value, attrs=None)
49 output += super(FormatPanel, self).render(name, value, attrs=None)
50
50
51 return output
51 return output
52
52
53
53
54 class PlainErrorList(ErrorList):
54 class PlainErrorList(ErrorList):
55 def __unicode__(self):
55 def __unicode__(self):
56 return self.as_text()
56 return self.as_text()
57
57
58 def as_text(self):
58 def as_text(self):
59 return ''.join([u'(!) %s ' % e for e in self])
59 return ''.join([u'(!) %s ' % e for e in self])
60
60
61
61
62 class NeboardForm(forms.Form):
62 class NeboardForm(forms.Form):
63
63
64 def as_div(self):
64 def as_div(self):
65 """
65 """
66 Returns this form rendered as HTML <as_div>s.
66 Returns this form rendered as HTML <as_div>s.
67 """
67 """
68
68
69 return self._html_output(
69 return self._html_output(
70 # TODO Do not show hidden rows in the list here
70 # TODO Do not show hidden rows in the list here
71 normal_row='<div class="form-row"><div class="form-label">'
71 normal_row='<div class="form-row"><div class="form-label">'
72 '%(label)s'
72 '%(label)s'
73 '</div></div>'
73 '</div></div>'
74 '<div class="form-row"><div class="form-input">'
74 '<div class="form-row"><div class="form-input">'
75 '%(field)s'
75 '%(field)s'
76 '</div></div>'
76 '</div></div>'
77 '<div class="form-row">'
77 '<div class="form-row">'
78 '%(help_text)s'
78 '%(help_text)s'
79 '</div>',
79 '</div>',
80 error_row='<div class="form-row">'
80 error_row='<div class="form-row">'
81 '<div class="form-label"></div>'
81 '<div class="form-label"></div>'
82 '<div class="form-errors">%s</div>'
82 '<div class="form-errors">%s</div>'
83 '</div>',
83 '</div>',
84 row_ender='</div>',
84 row_ender='</div>',
85 help_text_html='%s',
85 help_text_html='%s',
86 errors_on_separate_row=True)
86 errors_on_separate_row=True)
87
87
88 def as_json_errors(self):
88 def as_json_errors(self):
89 errors = []
89 errors = []
90
90
91 for name, field in self.fields.items():
91 for name, field in self.fields.items():
92 if self[name].errors:
92 if self[name].errors:
93 errors.append({
93 errors.append({
94 'field': name,
94 'field': name,
95 'errors': self[name].errors.as_text(),
95 'errors': self[name].errors.as_text(),
96 })
96 })
97
97
98 return errors
98 return errors
99
99
100
100
101 class PostForm(NeboardForm):
101 class PostForm(NeboardForm):
102
102
103 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
103 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
104 label=LABEL_TITLE)
104 label=LABEL_TITLE)
105 text = forms.CharField(
105 text = forms.CharField(
106 widget=FormatPanel(attrs={ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER}),
106 widget=FormatPanel(attrs={ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER}),
107 required=False, label=LABEL_TEXT)
107 required=False, label=LABEL_TEXT)
108 image = forms.ImageField(required=False, label=_('Image'),
108 image = forms.ImageField(required=False, label=_('Image'),
109 widget=forms.ClearableFileInput(attrs={'accept': 'image/*'}))
109 widget=forms.ClearableFileInput(attrs={'accept': 'image/*'}))
110
110
111 # This field is for spam prevention only
111 # This field is for spam prevention only
112 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
112 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
113 widget=forms.TextInput(attrs={
113 widget=forms.TextInput(attrs={
114 'class': 'form-email'}))
114 'class': 'form-email'}))
115
115
116 session = None
116 session = None
117 need_to_ban = False
117 need_to_ban = False
118
118
119 def clean_title(self):
119 def clean_title(self):
120 title = self.cleaned_data['title']
120 title = self.cleaned_data['title']
121 if title:
121 if title:
122 if len(title) > TITLE_MAX_LENGTH:
122 if len(title) > TITLE_MAX_LENGTH:
123 raise forms.ValidationError(_('Title must have less than %s '
123 raise forms.ValidationError(_('Title must have less than %s '
124 'characters') %
124 'characters') %
125 str(TITLE_MAX_LENGTH))
125 str(TITLE_MAX_LENGTH))
126 return title
126 return title
127
127
128 def clean_text(self):
128 def clean_text(self):
129 text = self.cleaned_data['text'].strip()
129 text = self.cleaned_data['text'].strip()
130 if text:
130 if text:
131 if len(text) > board_settings.MAX_TEXT_LENGTH:
131 if len(text) > board_settings.MAX_TEXT_LENGTH:
132 raise forms.ValidationError(_('Text must have less than %s '
132 raise forms.ValidationError(_('Text must have less than %s '
133 'characters') %
133 'characters') %
134 str(board_settings
134 str(board_settings
135 .MAX_TEXT_LENGTH))
135 .MAX_TEXT_LENGTH))
136 return text
136 return text
137
137
138 def clean_image(self):
138 def clean_image(self):
139 image = self.cleaned_data['image']
139 image = self.cleaned_data['image']
140 if image:
140 if image:
141 if image._size > board_settings.MAX_IMAGE_SIZE:
141 if image._size > board_settings.MAX_IMAGE_SIZE:
142 raise forms.ValidationError(
142 raise forms.ValidationError(
143 _('Image must be less than %s bytes')
143 _('Image must be less than %s bytes')
144 % str(board_settings.MAX_IMAGE_SIZE))
144 % str(board_settings.MAX_IMAGE_SIZE))
145
145
146 md5 = hashlib.md5()
146 md5 = hashlib.md5()
147 for chunk in image.chunks():
147 for chunk in image.chunks():
148 md5.update(chunk)
148 md5.update(chunk)
149 image_hash = md5.hexdigest()
149 image_hash = md5.hexdigest()
150 if Post.objects.filter(image_hash=image_hash).exists():
150 if PostImage.objects.filter(hash=image_hash).exists():
151 raise forms.ValidationError(ERROR_IMAGE_DUPLICATE)
151 raise forms.ValidationError(ERROR_IMAGE_DUPLICATE)
152
152
153 return image
153 return image
154
154
155 def clean(self):
155 def clean(self):
156 cleaned_data = super(PostForm, self).clean()
156 cleaned_data = super(PostForm, self).clean()
157
157
158 if not self.session:
158 if not self.session:
159 raise forms.ValidationError('Humans have sessions')
159 raise forms.ValidationError('Humans have sessions')
160
160
161 if cleaned_data['email']:
161 if cleaned_data['email']:
162 self.need_to_ban = True
162 self.need_to_ban = True
163 raise forms.ValidationError('A human cannot enter a hidden field')
163 raise forms.ValidationError('A human cannot enter a hidden field')
164
164
165 if not self.errors:
165 if not self.errors:
166 self._clean_text_image()
166 self._clean_text_image()
167
167
168 if not self.errors and self.session:
168 if not self.errors and self.session:
169 self._validate_posting_speed()
169 self._validate_posting_speed()
170
170
171 return cleaned_data
171 return cleaned_data
172
172
173 def _clean_text_image(self):
173 def _clean_text_image(self):
174 text = self.cleaned_data.get('text')
174 text = self.cleaned_data.get('text')
175 image = self.cleaned_data.get('image')
175 image = self.cleaned_data.get('image')
176
176
177 if (not text) and (not image):
177 if (not text) and (not image):
178 error_message = _('Either text or image must be entered.')
178 error_message = _('Either text or image must be entered.')
179 self._errors['text'] = self.error_class([error_message])
179 self._errors['text'] = self.error_class([error_message])
180
180
181 def _validate_posting_speed(self):
181 def _validate_posting_speed(self):
182 can_post = True
182 can_post = True
183
183
184 # TODO Remove this, it's only for test
184 # TODO Remove this, it's only for test
185 if not 'user_id' in self.session:
185 if not 'user_id' in self.session:
186 return
186 return
187
187
188 user = User.objects.get(id=self.session['user_id'])
188 user = User.objects.get(id=self.session['user_id'])
189 if user.is_veteran():
189 if user.is_veteran():
190 posting_delay = VETERAN_POSTING_DELAY
190 posting_delay = VETERAN_POSTING_DELAY
191 else:
191 else:
192 posting_delay = settings.POSTING_DELAY
192 posting_delay = settings.POSTING_DELAY
193
193
194 if LAST_POST_TIME in self.session:
194 if LAST_POST_TIME in self.session:
195 now = time.time()
195 now = time.time()
196 last_post_time = self.session[LAST_POST_TIME]
196 last_post_time = self.session[LAST_POST_TIME]
197
197
198 current_delay = int(now - last_post_time)
198 current_delay = int(now - last_post_time)
199
199
200 if current_delay < posting_delay:
200 if current_delay < posting_delay:
201 error_message = _('Wait %s seconds after last posting') % str(
201 error_message = _('Wait %s seconds after last posting') % str(
202 posting_delay - current_delay)
202 posting_delay - current_delay)
203 self._errors['text'] = self.error_class([error_message])
203 self._errors['text'] = self.error_class([error_message])
204
204
205 can_post = False
205 can_post = False
206
206
207 if can_post:
207 if can_post:
208 self.session[LAST_POST_TIME] = time.time()
208 self.session[LAST_POST_TIME] = time.time()
209
209
210
210
211 class ThreadForm(PostForm):
211 class ThreadForm(PostForm):
212
212
213 regex_tags = re.compile(ur'^[\w\s\d]+$', re.UNICODE)
213 regex_tags = re.compile(ur'^[\w\s\d]+$', re.UNICODE)
214
214
215 tags = forms.CharField(
215 tags = forms.CharField(
216 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
216 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
217 max_length=100, label=_('Tags'), required=True)
217 max_length=100, label=_('Tags'), required=True)
218
218
219 def clean_tags(self):
219 def clean_tags(self):
220 tags = self.cleaned_data['tags'].strip()
220 tags = self.cleaned_data['tags'].strip()
221
221
222 if not tags or not self.regex_tags.match(tags):
222 if not tags or not self.regex_tags.match(tags):
223 raise forms.ValidationError(
223 raise forms.ValidationError(
224 _('Inappropriate characters in tags.'))
224 _('Inappropriate characters in tags.'))
225
225
226 return tags
226 return tags
227
227
228 def clean(self):
228 def clean(self):
229 cleaned_data = super(ThreadForm, self).clean()
229 cleaned_data = super(ThreadForm, self).clean()
230
230
231 return cleaned_data
231 return cleaned_data
232
232
233
233
234 class PostCaptchaForm(PostForm):
234 class PostCaptchaForm(PostForm):
235 captcha = CaptchaField()
235 captcha = CaptchaField()
236
236
237 def __init__(self, *args, **kwargs):
237 def __init__(self, *args, **kwargs):
238 self.request = kwargs['request']
238 self.request = kwargs['request']
239 del kwargs['request']
239 del kwargs['request']
240
240
241 super(PostCaptchaForm, self).__init__(*args, **kwargs)
241 super(PostCaptchaForm, self).__init__(*args, **kwargs)
242
242
243 def clean(self):
243 def clean(self):
244 cleaned_data = super(PostCaptchaForm, self).clean()
244 cleaned_data = super(PostCaptchaForm, self).clean()
245
245
246 success = self.is_valid()
246 success = self.is_valid()
247 utils.update_captcha_access(self.request, success)
247 utils.update_captcha_access(self.request, success)
248
248
249 if success:
249 if success:
250 return cleaned_data
250 return cleaned_data
251 else:
251 else:
252 raise forms.ValidationError(_("Captcha validation failed"))
252 raise forms.ValidationError(_("Captcha validation failed"))
253
253
254
254
255 class ThreadCaptchaForm(ThreadForm):
255 class ThreadCaptchaForm(ThreadForm):
256 captcha = CaptchaField()
256 captcha = CaptchaField()
257
257
258 def __init__(self, *args, **kwargs):
258 def __init__(self, *args, **kwargs):
259 self.request = kwargs['request']
259 self.request = kwargs['request']
260 del kwargs['request']
260 del kwargs['request']
261
261
262 super(ThreadCaptchaForm, self).__init__(*args, **kwargs)
262 super(ThreadCaptchaForm, self).__init__(*args, **kwargs)
263
263
264 def clean(self):
264 def clean(self):
265 cleaned_data = super(ThreadCaptchaForm, self).clean()
265 cleaned_data = super(ThreadCaptchaForm, self).clean()
266
266
267 success = self.is_valid()
267 success = self.is_valid()
268 utils.update_captcha_access(self.request, success)
268 utils.update_captcha_access(self.request, success)
269
269
270 if success:
270 if success:
271 return cleaned_data
271 return cleaned_data
272 else:
272 else:
273 raise forms.ValidationError(_("Captcha validation failed"))
273 raise forms.ValidationError(_("Captcha validation failed"))
274
274
275
275
276 class SettingsForm(NeboardForm):
276 class SettingsForm(NeboardForm):
277
277
278 theme = forms.ChoiceField(choices=settings.THEMES,
278 theme = forms.ChoiceField(choices=settings.THEMES,
279 label=_('Theme'))
279 label=_('Theme'))
280
280
281
281
282 class ModeratorSettingsForm(SettingsForm):
282 class ModeratorSettingsForm(SettingsForm):
283
283
284 moderate = forms.BooleanField(required=False, label=_('Enable moderation '
284 moderate = forms.BooleanField(required=False, label=_('Enable moderation '
285 'panel'))
285 'panel'))
286
286
287
287
288 class LoginForm(NeboardForm):
288 class LoginForm(NeboardForm):
289
289
290 user_id = forms.CharField()
290 user_id = forms.CharField()
291
291
292 session = None
292 session = None
293
293
294 def clean_user_id(self):
294 def clean_user_id(self):
295 user_id = self.cleaned_data['user_id']
295 user_id = self.cleaned_data['user_id']
296 if user_id:
296 if user_id:
297 users = User.objects.filter(user_id=user_id)
297 users = User.objects.filter(user_id=user_id)
298 if len(users) == 0:
298 if len(users) == 0:
299 raise forms.ValidationError(_('No such user found'))
299 raise forms.ValidationError(_('No such user found'))
300
300
301 return user_id
301 return user_id
302
302
303 def _validate_login_speed(self):
303 def _validate_login_speed(self):
304 can_post = True
304 can_post = True
305
305
306 if LAST_LOGIN_TIME in self.session:
306 if LAST_LOGIN_TIME in self.session:
307 now = time.time()
307 now = time.time()
308 last_login_time = self.session[LAST_LOGIN_TIME]
308 last_login_time = self.session[LAST_LOGIN_TIME]
309
309
310 current_delay = int(now - last_login_time)
310 current_delay = int(now - last_login_time)
311
311
312 if current_delay < board_settings.LOGIN_TIMEOUT:
312 if current_delay < board_settings.LOGIN_TIMEOUT:
313 error_message = _('Wait %s minutes after last login') % str(
313 error_message = _('Wait %s minutes after last login') % str(
314 (board_settings.LOGIN_TIMEOUT - current_delay) / 60)
314 (board_settings.LOGIN_TIMEOUT - current_delay) / 60)
315 self._errors['user_id'] = self.error_class([error_message])
315 self._errors['user_id'] = self.error_class([error_message])
316
316
317 can_post = False
317 can_post = False
318
318
319 if can_post:
319 if can_post:
320 self.session[LAST_LOGIN_TIME] = time.time()
320 self.session[LAST_LOGIN_TIME] = time.time()
321
321
322 def clean(self):
322 def clean(self):
323 if not self.session:
323 if not self.session:
324 raise forms.ValidationError('Humans have sessions')
324 raise forms.ValidationError('Humans have sessions')
325
325
326 self._validate_login_speed()
326 self._validate_login_speed()
327
327
328 cleaned_data = super(LoginForm, self).clean()
328 cleaned_data = super(LoginForm, self).clean()
329
329
330 return cleaned_data
330 return cleaned_data
331
331
332
332
333 class AddTagForm(NeboardForm):
333 class AddTagForm(NeboardForm):
334
334
335 tag = forms.CharField(max_length=TAG_MAX_LENGTH, label=LABEL_TAG)
335 tag = forms.CharField(max_length=TAG_MAX_LENGTH, label=LABEL_TAG)
336 method = forms.CharField(widget=forms.HiddenInput(), initial='add_tag')
336 method = forms.CharField(widget=forms.HiddenInput(), initial='add_tag')
337
337
338 def clean_tag(self):
338 def clean_tag(self):
339 tag = self.cleaned_data['tag']
339 tag = self.cleaned_data['tag']
340
340
341 regex_tag = re.compile(REGEX_TAG, re.UNICODE)
341 regex_tag = re.compile(REGEX_TAG, re.UNICODE)
342 if not regex_tag.match(tag):
342 if not regex_tag.match(tag):
343 raise forms.ValidationError(_('Inappropriate characters in tags.'))
343 raise forms.ValidationError(_('Inappropriate characters in tags.'))
344
344
345 return tag
345 return tag
346
346
347 def clean(self):
347 def clean(self):
348 cleaned_data = super(AddTagForm, self).clean()
348 cleaned_data = super(AddTagForm, self).clean()
349
349
350 return cleaned_data
350 return cleaned_data
351
351
1 NO CONTENT: modified file, binary diff hidden
NO CONTENT: modified file, binary diff hidden
@@ -1,390 +1,390 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-05-08 21:35+0300\n"
10 "POT-Creation-Date: 2014-06-15 12:34+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:5
22 msgid "author"
22 msgid "author"
23 msgstr "Π°Π²Ρ‚ΠΎΡ€"
23 msgstr "Π°Π²Ρ‚ΠΎΡ€"
24
24
25 #: authors.py:6
25 #: authors.py:6
26 msgid "developer"
26 msgid "developer"
27 msgstr "Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ"
27 msgstr "Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ"
28
28
29 #: authors.py:7
29 #: authors.py:7
30 msgid "javascript developer"
30 msgid "javascript developer"
31 msgstr "Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ javascript"
31 msgstr "Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ javascript"
32
32
33 #: authors.py:8
33 #: authors.py:8
34 msgid "designer"
34 msgid "designer"
35 msgstr "Π΄ΠΈΠ·Π°ΠΉΠ½Π΅Ρ€"
35 msgstr "Π΄ΠΈΠ·Π°ΠΉΠ½Π΅Ρ€"
36
36
37 #: forms.py:23
37 #: forms.py:23
38 msgid ""
38 msgid ""
39 "Type message here. You can reply to message >>123 like\n"
39 "Type message here. You can reply to message >>123 like\n"
40 " this. 2 new lines are required to start new paragraph."
40 " this. 2 new lines are required to start new paragraph."
41 msgstr ""
41 msgstr ""
42 "Π’Π²Π΅Π΄ΠΈΡ‚Π΅ сообщСниС здСсь. Π’Ρ‹ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ ΠΎΡ‚Π²Π΅Ρ‚ΠΈΡ‚ΡŒ Π½Π° сообщСниС >>123 Π²ΠΎΡ‚ Ρ‚Π°ΠΊ. 2 "
42 "Π’Π²Π΅Π΄ΠΈΡ‚Π΅ сообщСниС здСсь. Π’Ρ‹ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ ΠΎΡ‚Π²Π΅Ρ‚ΠΈΡ‚ΡŒ Π½Π° сообщСниС >>123 Π²ΠΎΡ‚ Ρ‚Π°ΠΊ. 2 "
43 "пСрСноса строки ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹ для создания Π½ΠΎΠ²ΠΎΠ³ΠΎ Π°Π±Π·Π°Ρ†Π°."
43 "пСрСноса строки ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹ для создания Π½ΠΎΠ²ΠΎΠ³ΠΎ Π°Π±Π·Π°Ρ†Π°."
44
44
45 #: forms.py:25
45 #: forms.py:25
46 msgid "tag1 several_words_tag"
46 msgid "tag1 several_words_tag"
47 msgstr "Ρ‚Π΅Π³1 Ρ‚Π΅Π³_ΠΈΠ·_Π½Π΅ΡΠΊΠΎΠ»ΡŒΠΊΠΈΡ…_слов"
47 msgstr "Ρ‚Π΅Π³1 Ρ‚Π΅Π³_ΠΈΠ·_Π½Π΅ΡΠΊΠΎΠ»ΡŒΠΊΠΈΡ…_слов"
48
48
49 #: forms.py:27
49 #: forms.py:27
50 msgid "Such image was already posted"
50 msgid "Such image was already posted"
51 msgstr "Π’Π°ΠΊΠΎΠ΅ ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ ΡƒΠΆΠ΅ Π±Ρ‹Π»ΠΎ Π·Π°Π³Ρ€ΡƒΠΆΠ΅Π½ΠΎ"
51 msgstr "Π’Π°ΠΊΠΎΠ΅ ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ ΡƒΠΆΠ΅ Π±Ρ‹Π»ΠΎ Π·Π°Π³Ρ€ΡƒΠΆΠ΅Π½ΠΎ"
52
52
53 #: forms.py:29
53 #: forms.py:29
54 msgid "Title"
54 msgid "Title"
55 msgstr "Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ"
55 msgstr "Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ"
56
56
57 #: forms.py:30
57 #: forms.py:30
58 msgid "Text"
58 msgid "Text"
59 msgstr "ВСкст"
59 msgstr "ВСкст"
60
60
61 #: forms.py:31
61 #: forms.py:31
62 msgid "Tag"
62 msgid "Tag"
63 msgstr "Π’Π΅Π³"
63 msgstr "Π’Π΅Π³"
64
64
65 #: forms.py:108
65 #: forms.py:108
66 msgid "Image"
66 msgid "Image"
67 msgstr "Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅"
67 msgstr "Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅"
68
68
69 #: forms.py:112
69 #: forms.py:112
70 msgid "e-mail"
70 msgid "e-mail"
71 msgstr ""
71 msgstr ""
72
72
73 #: forms.py:123
73 #: forms.py:123
74 #, python-format
74 #, python-format
75 msgid "Title must have less than %s characters"
75 msgid "Title must have less than %s characters"
76 msgstr "Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ Π΄ΠΎΠ»ΠΆΠ΅Π½ ΠΈΠΌΠ΅Ρ‚ΡŒ мСньшС %s символов"
76 msgstr "Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ Π΄ΠΎΠ»ΠΆΠ΅Π½ ΠΈΠΌΠ΅Ρ‚ΡŒ мСньшС %s символов"
77
77
78 #: forms.py:132
78 #: forms.py:132
79 #, python-format
79 #, python-format
80 msgid "Text must have less than %s characters"
80 msgid "Text must have less than %s characters"
81 msgstr "ВСкст Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±Ρ‹Ρ‚ΡŒ ΠΊΠΎΡ€ΠΎΡ‡Π΅ %s символов"
81 msgstr "ВСкст Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±Ρ‹Ρ‚ΡŒ ΠΊΠΎΡ€ΠΎΡ‡Π΅ %s символов"
82
82
83 #: forms.py:143
83 #: forms.py:143
84 #, python-format
84 #, python-format
85 msgid "Image must be less than %s bytes"
85 msgid "Image must be less than %s bytes"
86 msgstr "Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ Π΄ΠΎΠ»ΠΆΠ½ΠΎ Π±Ρ‹Ρ‚ΡŒ ΠΌΠ΅Π½Π΅Π΅ %s Π±Π°ΠΉΡ‚"
86 msgstr "Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ Π΄ΠΎΠ»ΠΆΠ½ΠΎ Π±Ρ‹Ρ‚ΡŒ ΠΌΠ΅Π½Π΅Π΅ %s Π±Π°ΠΉΡ‚"
87
87
88 #: forms.py:178
88 #: forms.py:178
89 msgid "Either text or image must be entered."
89 msgid "Either text or image must be entered."
90 msgstr "ВСкст ΠΈΠ»ΠΈ ΠΊΠ°Ρ€Ρ‚ΠΈΠ½ΠΊΠ° Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ Π²Π²Π΅Π΄Π΅Π½Ρ‹."
90 msgstr "ВСкст ΠΈΠ»ΠΈ ΠΊΠ°Ρ€Ρ‚ΠΈΠ½ΠΊΠ° Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ Π²Π²Π΅Π΄Π΅Π½Ρ‹."
91
91
92 #: forms.py:201
92 #: forms.py:201
93 #, python-format
93 #, python-format
94 msgid "Wait %s seconds after last posting"
94 msgid "Wait %s seconds after last posting"
95 msgstr "ΠŸΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡ‚Π΅ %s сСкунд послС послСднСго постинга"
95 msgstr "ΠŸΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡ‚Π΅ %s сСкунд послС послСднСго постинга"
96
96
97 #: forms.py:217 templates/boards/tags.html:7 templates/boards/rss/post.html:10
97 #: forms.py:217 templates/boards/tags.html:7 templates/boards/rss/post.html:10
98 msgid "Tags"
98 msgid "Tags"
99 msgstr "Π’Π΅Π³ΠΈ"
99 msgstr "Π’Π΅Π³ΠΈ"
100
100
101 #: forms.py:225 forms.py:344
101 #: forms.py:224 forms.py:343
102 msgid "Inappropriate characters in tags."
102 msgid "Inappropriate characters in tags."
103 msgstr "НСдопустимыС символы Π² Ρ‚Π΅Π³Π°Ρ…."
103 msgstr "НСдопустимыС символы Π² Ρ‚Π΅Π³Π°Ρ…."
104
104
105 #: forms.py:253 forms.py:274
105 #: forms.py:252 forms.py:273
106 msgid "Captcha validation failed"
106 msgid "Captcha validation failed"
107 msgstr "ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° ΠΊΠ°ΠΏΡ‡ΠΈ ΠΏΡ€ΠΎΠ²Π°Π»Π΅Π½Π°"
107 msgstr "ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° ΠΊΠ°ΠΏΡ‡ΠΈ ΠΏΡ€ΠΎΠ²Π°Π»Π΅Π½Π°"
108
108
109 #: forms.py:280
109 #: forms.py:279
110 msgid "Theme"
110 msgid "Theme"
111 msgstr "Π’Π΅ΠΌΠ°"
111 msgstr "Π’Π΅ΠΌΠ°"
112
112
113 #: forms.py:285
113 #: forms.py:284
114 msgid "Enable moderation panel"
114 msgid "Enable moderation panel"
115 msgstr "Π’ΠΊΠ»ΡŽΡ‡ΠΈΡ‚ΡŒ панСль ΠΌΠΎΠ΄Π΅Ρ€Π°Ρ†ΠΈΠΈ"
115 msgstr "Π’ΠΊΠ»ΡŽΡ‡ΠΈΡ‚ΡŒ панСль ΠΌΠΎΠ΄Π΅Ρ€Π°Ρ†ΠΈΠΈ"
116
116
117 #: forms.py:300
117 #: forms.py:299
118 msgid "No such user found"
118 msgid "No such user found"
119 msgstr "Π”Π°Π½Π½Ρ‹ΠΉ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½"
119 msgstr "Π”Π°Π½Π½Ρ‹ΠΉ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½"
120
120
121 #: forms.py:314
121 #: forms.py:313
122 #, python-format
122 #, python-format
123 msgid "Wait %s minutes after last login"
123 msgid "Wait %s minutes after last login"
124 msgstr "ΠŸΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡ‚Π΅ %s ΠΌΠΈΠ½ΡƒΡ‚ послС послСднСго Π²Ρ…ΠΎΠ΄Π°"
124 msgstr "ΠŸΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡ‚Π΅ %s ΠΌΠΈΠ½ΡƒΡ‚ послС послСднСго Π²Ρ…ΠΎΠ΄Π°"
125
125
126 #: templates/boards/404.html:6
126 #: templates/boards/404.html:6
127 msgid "Not found"
127 msgid "Not found"
128 msgstr "НС найдСно"
128 msgstr "НС найдСно"
129
129
130 #: templates/boards/404.html:12
130 #: templates/boards/404.html:12
131 msgid "This page does not exist"
131 msgid "This page does not exist"
132 msgstr "Π­Ρ‚ΠΎΠΉ страницы Π½Π΅ сущСствуСт"
132 msgstr "Π­Ρ‚ΠΎΠΉ страницы Π½Π΅ сущСствуСт"
133
133
134 #: templates/boards/authors.html:6 templates/boards/authors.html.py:12
134 #: templates/boards/authors.html:6 templates/boards/authors.html.py:12
135 msgid "Authors"
135 msgid "Authors"
136 msgstr "Авторы"
136 msgstr "Авторы"
137
137
138 #: templates/boards/authors.html:26
138 #: templates/boards/authors.html:26
139 msgid "Distributed under the"
139 msgid "Distributed under the"
140 msgstr "РаспространяСтся ΠΏΠΎΠ΄"
140 msgstr "РаспространяСтся ΠΏΠΎΠ΄"
141
141
142 #: templates/boards/authors.html:28
142 #: templates/boards/authors.html:28
143 msgid "license"
143 msgid "license"
144 msgstr "Π»ΠΈΡ†Π΅Π½Π·ΠΈΠ΅ΠΉ"
144 msgstr "Π»ΠΈΡ†Π΅Π½Π·ΠΈΠ΅ΠΉ"
145
145
146 #: templates/boards/authors.html:30
146 #: templates/boards/authors.html:30
147 msgid "Repository"
147 msgid "Repository"
148 msgstr "Π Π΅ΠΏΠΎΠ·ΠΈΡ‚ΠΎΡ€ΠΈΠΉ"
148 msgstr "Π Π΅ΠΏΠΎΠ·ΠΈΡ‚ΠΎΡ€ΠΈΠΉ"
149
149
150 #: templates/boards/base.html:14
150 #: templates/boards/base.html:11
151 msgid "Feed"
151 msgid "Feed"
152 msgstr "Π›Π΅Π½Ρ‚Π°"
152 msgstr "Π›Π΅Π½Ρ‚Π°"
153
153
154 #: templates/boards/base.html:31
154 #: templates/boards/base.html:28
155 msgid "All threads"
155 msgid "All threads"
156 msgstr "ВсС Ρ‚Π΅ΠΌΡ‹"
156 msgstr "ВсС Ρ‚Π΅ΠΌΡ‹"
157
157
158 #: templates/boards/base.html:36
158 #: templates/boards/base.html:33
159 msgid "Tag management"
159 msgid "Tag management"
160 msgstr "Π£ΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠ΅ Ρ‚Π΅Π³Π°ΠΌΠΈ"
160 msgstr "Π£ΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠ΅ Ρ‚Π΅Π³Π°ΠΌΠΈ"
161
161
162 #: templates/boards/base.html:38 templates/boards/settings.html:7
162 #: templates/boards/base.html:35 templates/boards/settings.html:7
163 msgid "Settings"
163 msgid "Settings"
164 msgstr "Настройки"
164 msgstr "Настройки"
165
165
166 #: templates/boards/base.html:50 templates/boards/login.html:6
166 #: templates/boards/base.html:47 templates/boards/login.html:6
167 #: templates/boards/login.html.py:21
167 #: templates/boards/login.html.py:16
168 msgid "Login"
168 msgid "Login"
169 msgstr "Π’Ρ…ΠΎΠ΄"
169 msgstr "Π’Ρ…ΠΎΠ΄"
170
170
171 #: templates/boards/base.html:52
171 #: templates/boards/base.html:48
172 msgid "Search"
173 msgstr "Поиск"
174
175 #: templates/boards/base.html:50
172 #, python-format
176 #, python-format
173 msgid "Speed: %(ppd)s posts per day"
177 msgid "Speed: %(ppd)s posts per day"
174 msgstr "Π‘ΠΊΠΎΡ€ΠΎΡΡ‚ΡŒ: %(ppd)s сообщСний Π² дСнь"
178 msgstr "Π‘ΠΊΠΎΡ€ΠΎΡΡ‚ΡŒ: %(ppd)s сообщСний Π² дСнь"
175
179
176 #: templates/boards/base.html:54
180 #: templates/boards/base.html:52
177 msgid "Up"
181 msgid "Up"
178 msgstr "Π’Π²Π΅Ρ€Ρ…"
182 msgstr "Π’Π²Π΅Ρ€Ρ…"
179
183
180 #: templates/boards/login.html:15
184 #: templates/boards/login.html:19
181 msgid "User ID"
182 msgstr "ID ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ"
183
184 #: templates/boards/login.html:24
185 msgid "Insert your user id above"
185 msgid "Insert your user id above"
186 msgstr "Π’ΡΡ‚Π°Π²ΡŒΡ‚Π΅ свой ID ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ Π²Ρ‹ΡˆΠ΅"
186 msgstr "Π’ΡΡ‚Π°Π²ΡŒΡ‚Π΅ свой ID ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ Π²Ρ‹ΡˆΠ΅"
187
187
188 #: templates/boards/post.html:21 templates/boards/staticpages/help.html:19
188 #: templates/boards/post.html:21 templates/boards/staticpages/help.html:19
189 msgid "Quote"
189 msgid "Quote"
190 msgstr "Π¦ΠΈΡ‚Π°Ρ‚Π°"
190 msgstr "Π¦ΠΈΡ‚Π°Ρ‚Π°"
191
191
192 #: templates/boards/post.html:31
192 #: templates/boards/post.html:31
193 msgid "Open"
193 msgid "Open"
194 msgstr "ΠžΡ‚ΠΊΡ€Ρ‹Ρ‚ΡŒ"
194 msgstr "ΠžΡ‚ΠΊΡ€Ρ‹Ρ‚ΡŒ"
195
195
196 #: templates/boards/post.html:33
196 #: templates/boards/post.html:33
197 msgid "Reply"
197 msgid "Reply"
198 msgstr "ΠžΡ‚Π²Π΅Ρ‚"
198 msgstr "ΠžΡ‚Π²Π΅Ρ‚"
199
199
200 #: templates/boards/post.html:40
200 #: templates/boards/post.html:40
201 msgid "Edit"
201 msgid "Edit"
202 msgstr "Π˜Π·ΠΌΠ΅Π½ΠΈΡ‚ΡŒ"
202 msgstr "Π˜Π·ΠΌΠ΅Π½ΠΈΡ‚ΡŒ"
203
203
204 #: templates/boards/post.html:42
204 #: templates/boards/post.html:42
205 msgid "Delete"
205 msgid "Delete"
206 msgstr "Π£Π΄Π°Π»ΠΈΡ‚ΡŒ"
206 msgstr "Π£Π΄Π°Π»ΠΈΡ‚ΡŒ"
207
207
208 #: templates/boards/post.html:45
208 #: templates/boards/post.html:45
209 msgid "Ban IP"
209 msgid "Ban IP"
210 msgstr "Π—Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ IP"
210 msgstr "Π—Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ IP"
211
211
212 #: templates/boards/post.html:74
212 #: templates/boards/post.html:74
213 msgid "Replies"
213 msgid "Replies"
214 msgstr "ΠžΡ‚Π²Π΅Ρ‚Ρ‹"
214 msgstr "ΠžΡ‚Π²Π΅Ρ‚Ρ‹"
215
215
216 #: templates/boards/post.html:84 templates/boards/thread.html:102
216 #: templates/boards/post.html:84 templates/boards/thread.html:102
217 #: templates/boards/thread_gallery.html:60
217 #: templates/boards/thread_gallery.html:60
218 msgid "images"
218 msgid "images"
219 msgstr "ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ"
219 msgstr "ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ"
220
220
221 #: templates/boards/post_admin.html:19
221 #: templates/boards/post_admin.html:19
222 msgid "Tags:"
222 msgid "Tags:"
223 msgstr "Π’Π΅Π³ΠΈ:"
223 msgstr "Π’Π΅Π³ΠΈ:"
224
224
225 #: templates/boards/post_admin.html:30
225 #: templates/boards/post_admin.html:30
226 msgid "Add tag"
226 msgid "Add tag"
227 msgstr "Π”ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ Ρ‚Π΅Π³"
227 msgstr "Π”ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ Ρ‚Π΅Π³"
228
228
229 #: templates/boards/posting_general.html:56
229 #: templates/boards/posting_general.html:56
230 msgid "Show tag"
230 msgid "Show tag"
231 msgstr "ΠŸΠΎΠΊΠ°Π·Ρ‹Π²Π°Ρ‚ΡŒ Ρ‚Π΅Π³"
231 msgstr "ΠŸΠΎΠΊΠ°Π·Ρ‹Π²Π°Ρ‚ΡŒ Ρ‚Π΅Π³"
232
232
233 #: templates/boards/posting_general.html:60
233 #: templates/boards/posting_general.html:60
234 msgid "Hide tag"
234 msgid "Hide tag"
235 msgstr "Π‘ΠΊΡ€Ρ‹Π²Π°Ρ‚ΡŒ Ρ‚Π΅Π³"
235 msgstr "Π‘ΠΊΡ€Ρ‹Π²Π°Ρ‚ΡŒ Ρ‚Π΅Π³"
236
236
237 #: templates/boards/posting_general.html:79
237 #: templates/boards/posting_general.html:79
238 msgid "Previous page"
238 msgid "Previous page"
239 msgstr "ΠŸΡ€Π΅Π΄Ρ‹Π΄ΡƒΡ‰Π°Ρ страница"
239 msgstr "ΠŸΡ€Π΅Π΄Ρ‹Π΄ΡƒΡ‰Π°Ρ страница"
240
240
241 #: templates/boards/posting_general.html:94
241 #: templates/boards/posting_general.html:94
242 #, python-format
242 #, python-format
243 msgid "Skipped %(count)s replies. Open thread to see all replies."
243 msgid "Skipped %(count)s replies. Open thread to see all replies."
244 msgstr "ΠŸΡ€ΠΎΠΏΡƒΡ‰Π΅Π½ΠΎ %(count)s ΠΎΡ‚Π²Π΅Ρ‚ΠΎΠ². ΠžΡ‚ΠΊΡ€ΠΎΠΉΡ‚Π΅ Ρ‚Ρ€Π΅Π΄, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΡƒΠ²ΠΈΠ΄Π΅Ρ‚ΡŒ всС ΠΎΡ‚Π²Π΅Ρ‚Ρ‹."
244 msgstr "ΠŸΡ€ΠΎΠΏΡƒΡ‰Π΅Π½ΠΎ %(count)s ΠΎΡ‚Π²Π΅Ρ‚ΠΎΠ². ΠžΡ‚ΠΊΡ€ΠΎΠΉΡ‚Π΅ Ρ‚Ρ€Π΅Π΄, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΡƒΠ²ΠΈΠ΄Π΅Ρ‚ΡŒ всС ΠΎΡ‚Π²Π΅Ρ‚Ρ‹."
245
245
246 #: templates/boards/posting_general.html:121
246 #: templates/boards/posting_general.html:121
247 msgid "Next page"
247 msgid "Next page"
248 msgstr "Π‘Π»Π΅Π΄ΡƒΡŽΡ‰Π°Ρ страница"
248 msgstr "Π‘Π»Π΅Π΄ΡƒΡŽΡ‰Π°Ρ страница"
249
249
250 #: templates/boards/posting_general.html:126
250 #: templates/boards/posting_general.html:126
251 msgid "No threads exist. Create the first one!"
251 msgid "No threads exist. Create the first one!"
252 msgstr "НСт Ρ‚Π΅ΠΌ. Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ ΠΏΠ΅Ρ€Π²ΡƒΡŽ!"
252 msgstr "НСт Ρ‚Π΅ΠΌ. Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ ΠΏΠ΅Ρ€Π²ΡƒΡŽ!"
253
253
254 #: templates/boards/posting_general.html:132
254 #: templates/boards/posting_general.html:132
255 msgid "Create new thread"
255 msgid "Create new thread"
256 msgstr "Π‘ΠΎΠ·Π΄Π°Ρ‚ΡŒ Π½ΠΎΠ²ΡƒΡŽ Ρ‚Π΅ΠΌΡƒ"
256 msgstr "Π‘ΠΎΠ·Π΄Π°Ρ‚ΡŒ Π½ΠΎΠ²ΡƒΡŽ Ρ‚Π΅ΠΌΡƒ"
257
257
258 #: templates/boards/posting_general.html:137 templates/boards/thread.html:62
258 #: templates/boards/posting_general.html:137 templates/boards/thread.html:62
259 #: templates/boards/thread.html.py:71
259 #: templates/boards/thread.html.py:71
260 msgid "Post"
260 msgid "Post"
261 msgstr "ΠžΡ‚ΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ"
261 msgstr "ΠžΡ‚ΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ"
262
262
263 #: templates/boards/posting_general.html:142
263 #: templates/boards/posting_general.html:142
264 msgid "Tags must be delimited by spaces. Text or image is required."
264 msgid "Tags must be delimited by spaces. Text or image is required."
265 msgstr ""
265 msgstr ""
266 "Π’Π΅Π³ΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ Ρ€Π°Π·Π΄Π΅Π»Π΅Π½Ρ‹ ΠΏΡ€ΠΎΠ±Π΅Π»Π°ΠΌΠΈ. ВСкст ΠΈΠ»ΠΈ ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹."
266 "Π’Π΅Π³ΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ Ρ€Π°Π·Π΄Π΅Π»Π΅Π½Ρ‹ ΠΏΡ€ΠΎΠ±Π΅Π»Π°ΠΌΠΈ. ВСкст ΠΈΠ»ΠΈ ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹."
267
267
268 #: templates/boards/posting_general.html:145 templates/boards/thread.html:79
268 #: templates/boards/posting_general.html:145 templates/boards/thread.html:79
269 msgid "Text syntax"
269 msgid "Text syntax"
270 msgstr "Бинтаксис тСкста"
270 msgstr "Бинтаксис тСкста"
271
271
272 #: templates/boards/posting_general.html:157
272 #: templates/boards/posting_general.html:157
273 msgid "Pages:"
273 msgid "Pages:"
274 msgstr "Π‘Ρ‚Ρ€Π°Π½ΠΈΡ†Ρ‹: "
274 msgstr "Π‘Ρ‚Ρ€Π°Π½ΠΈΡ†Ρ‹: "
275
275
276 #: templates/boards/settings.html:14
276 #: templates/boards/settings.html:14
277 msgid "User:"
277 msgid "User:"
278 msgstr "ΠŸΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ:"
278 msgstr "ΠŸΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ:"
279
279
280 #: templates/boards/settings.html:16
280 #: templates/boards/settings.html:16
281 msgid "You are moderator."
281 msgid "You are moderator."
282 msgstr "Π’Ρ‹ ΠΌΠΎΠ΄Π΅Ρ€Π°Ρ‚ΠΎΡ€."
282 msgstr "Π’Ρ‹ ΠΌΠΎΠ΄Π΅Ρ€Π°Ρ‚ΠΎΡ€."
283
283
284 #: templates/boards/settings.html:19
284 #: templates/boards/settings.html:19
285 msgid "You are veteran."
285 msgid "You are veteran."
286 msgstr "Π’Ρ‹ Π²Π΅Ρ‚Π΅Ρ€Π°Π½."
286 msgstr "Π’Ρ‹ Π²Π΅Ρ‚Π΅Ρ€Π°Π½."
287
287
288 #: templates/boards/settings.html:22
288 #: templates/boards/settings.html:22
289 msgid "Posts:"
289 msgid "Posts:"
290 msgstr "Π‘ΠΎΠΎΠ±Ρ‰Π΅Π½ΠΈΠΉ:"
290 msgstr "Π‘ΠΎΠΎΠ±Ρ‰Π΅Π½ΠΈΠΉ:"
291
291
292 #: templates/boards/settings.html:23
292 #: templates/boards/settings.html:23
293 msgid "First access:"
293 msgid "First access:"
294 msgstr "ΠŸΠ΅Ρ€Π²Ρ‹ΠΉ доступ:"
294 msgstr "ΠŸΠ΅Ρ€Π²Ρ‹ΠΉ доступ:"
295
295
296 #: templates/boards/settings.html:25
296 #: templates/boards/settings.html:25
297 msgid "Last access:"
297 msgid "Last access:"
298 msgstr "ПослСдний доступ: "
298 msgstr "ПослСдний доступ: "
299
299
300 #: templates/boards/settings.html:29
300 #: templates/boards/settings.html:29
301 msgid "Hidden tags:"
301 msgid "Hidden tags:"
302 msgstr "Π‘ΠΊΡ€Ρ‹Ρ‚Ρ‹Π΅ Ρ‚Π΅Π³ΠΈ:"
302 msgstr "Π‘ΠΊΡ€Ρ‹Ρ‚Ρ‹Π΅ Ρ‚Π΅Π³ΠΈ:"
303
303
304 #: templates/boards/settings.html:44
304 #: templates/boards/settings.html:44
305 msgid "Save"
305 msgid "Save"
306 msgstr "Π‘ΠΎΡ…Ρ€Π°Π½ΠΈΡ‚ΡŒ"
306 msgstr "Π‘ΠΎΡ…Ρ€Π°Π½ΠΈΡ‚ΡŒ"
307
307
308 #: templates/boards/tags.html:22
308 #: templates/boards/tags.html:22
309 msgid "No tags found."
309 msgid "No tags found."
310 msgstr "Π’Π΅Π³ΠΈ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½Ρ‹."
310 msgstr "Π’Π΅Π³ΠΈ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½Ρ‹."
311
311
312 #: templates/boards/thread.html:20 templates/boards/thread_gallery.html:21
312 #: templates/boards/thread.html:20 templates/boards/thread_gallery.html:21
313 msgid "Normal mode"
313 msgid "Normal mode"
314 msgstr "ΠΠΎΡ€ΠΌΠ°Π»ΡŒΠ½Ρ‹ΠΉ Ρ€Π΅ΠΆΠΈΠΌ"
314 msgstr "ΠΠΎΡ€ΠΌΠ°Π»ΡŒΠ½Ρ‹ΠΉ Ρ€Π΅ΠΆΠΈΠΌ"
315
315
316 #: templates/boards/thread.html:21 templates/boards/thread_gallery.html:22
316 #: templates/boards/thread.html:21 templates/boards/thread_gallery.html:22
317 msgid "Gallery mode"
317 msgid "Gallery mode"
318 msgstr "Π Π΅ΠΆΠΈΠΌ Π³Π°Π»Π΅Ρ€Π΅ΠΈ"
318 msgstr "Π Π΅ΠΆΠΈΠΌ Π³Π°Π»Π΅Ρ€Π΅ΠΈ"
319
319
320 #: templates/boards/thread.html:29
320 #: templates/boards/thread.html:29
321 msgid "posts to bumplimit"
321 msgid "posts to bumplimit"
322 msgstr "сообщСний Π΄ΠΎ Π±Π°ΠΌΠΏΠ»ΠΈΠΌΠΈΡ‚Π°"
322 msgstr "сообщСний Π΄ΠΎ Π±Π°ΠΌΠΏΠ»ΠΈΠΌΠΈΡ‚Π°"
323
323
324 #: templates/boards/thread.html:50
324 #: templates/boards/thread.html:50
325 msgid "Reply to thread"
325 msgid "Reply to thread"
326 msgstr "ΠžΡ‚Π²Π΅Ρ‚ΠΈΡ‚ΡŒ Π² Ρ‚Π΅ΠΌΡƒ"
326 msgstr "ΠžΡ‚Π²Π΅Ρ‚ΠΈΡ‚ΡŒ Π² Ρ‚Π΅ΠΌΡƒ"
327
327
328 #: templates/boards/thread.html:76
328 #: templates/boards/thread.html:76
329 msgid "Switch mode"
329 msgid "Switch mode"
330 msgstr "ΠŸΠ΅Ρ€Π΅ΠΊΠ»ΡŽΡ‡ΠΈΡ‚ΡŒ Ρ€Π΅ΠΆΠΈΠΌ"
330 msgstr "ΠŸΠ΅Ρ€Π΅ΠΊΠ»ΡŽΡ‡ΠΈΡ‚ΡŒ Ρ€Π΅ΠΆΠΈΠΌ"
331
331
332 #: templates/boards/thread.html:101 templates/boards/thread_gallery.html:59
332 #: templates/boards/thread.html:101 templates/boards/thread_gallery.html:59
333 msgid "replies"
333 msgid "replies"
334 msgstr "ΠΎΡ‚Π²Π΅Ρ‚ΠΎΠ²"
334 msgstr "ΠΎΡ‚Π²Π΅Ρ‚ΠΎΠ²"
335
335
336 #: templates/boards/thread.html:103 templates/boards/thread_gallery.html:61
336 #: templates/boards/thread.html:103 templates/boards/thread_gallery.html:61
337 msgid "Last update: "
337 msgid "Last update: "
338 msgstr "ПослСднСС обновлСниС: "
338 msgstr "ПослСднСС обновлСниС: "
339
339
340 #: templates/boards/rss/post.html:5
340 #: templates/boards/rss/post.html:5
341 msgid "Post image"
341 msgid "Post image"
342 msgstr "Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ сообщСния"
342 msgstr "Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ сообщСния"
343
343
344 #: templates/boards/staticpages/banned.html:6
344 #: templates/boards/staticpages/banned.html:6
345 msgid "Banned"
345 msgid "Banned"
346 msgstr "Π—Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Π½"
346 msgstr "Π—Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Π½"
347
347
348 #: templates/boards/staticpages/banned.html:11
348 #: templates/boards/staticpages/banned.html:11
349 msgid "Your IP address has been banned. Contact the administrator"
349 msgid "Your IP address has been banned. Contact the administrator"
350 msgstr "Π’Π°Ρˆ IP адрСс Π±Ρ‹Π» Π·Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Π½. Π‘Π²ΡΠΆΠΈΡ‚Π΅ΡΡŒ с администратором"
350 msgstr "Π’Π°Ρˆ IP адрСс Π±Ρ‹Π» Π·Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Π½. Π‘Π²ΡΠΆΠΈΡ‚Π΅ΡΡŒ с администратором"
351
351
352 #: templates/boards/staticpages/help.html:6
352 #: templates/boards/staticpages/help.html:6
353 #: templates/boards/staticpages/help.html:10
353 #: templates/boards/staticpages/help.html:10
354 msgid "Syntax"
354 msgid "Syntax"
355 msgstr "Бинтаксис"
355 msgstr "Бинтаксис"
356
356
357 #: templates/boards/staticpages/help.html:11
357 #: templates/boards/staticpages/help.html:11
358 msgid "2 line breaks for a new line."
358 msgid "2 line breaks for a new line."
359 msgstr "2 ΠΏΠ΅Ρ€Π΅Π²ΠΎΠ΄Π° строки ΡΠΎΠ·Π΄Π°ΡŽΡ‚ Π½ΠΎΠ²Ρ‹ΠΉ Π°Π±Π·Π°Ρ†."
359 msgstr "2 ΠΏΠ΅Ρ€Π΅Π²ΠΎΠ΄Π° строки ΡΠΎΠ·Π΄Π°ΡŽΡ‚ Π½ΠΎΠ²Ρ‹ΠΉ Π°Π±Π·Π°Ρ†."
360
360
361 #: templates/boards/staticpages/help.html:12
361 #: templates/boards/staticpages/help.html:12
362 msgid "Italic text"
362 msgid "Italic text"
363 msgstr "ΠšΡƒΡ€ΡΠΈΠ²Π½Ρ‹ΠΉ тСкст"
363 msgstr "ΠšΡƒΡ€ΡΠΈΠ²Π½Ρ‹ΠΉ тСкст"
364
364
365 #: templates/boards/staticpages/help.html:13
365 #: templates/boards/staticpages/help.html:13
366 msgid "Bold text"
366 msgid "Bold text"
367 msgstr "ΠŸΠΎΠ»ΡƒΠΆΠΈΡ€Π½Ρ‹ΠΉ тСкст"
367 msgstr "ΠŸΠΎΠ»ΡƒΠΆΠΈΡ€Π½Ρ‹ΠΉ тСкст"
368
368
369 #: templates/boards/staticpages/help.html:14
369 #: templates/boards/staticpages/help.html:14
370 msgid "Spoiler"
370 msgid "Spoiler"
371 msgstr "Π‘ΠΏΠΎΠΉΠ»Π΅Ρ€"
371 msgstr "Π‘ΠΏΠΎΠΉΠ»Π΅Ρ€"
372
372
373 #: templates/boards/staticpages/help.html:15
373 #: templates/boards/staticpages/help.html:15
374 msgid "Link to a post"
374 msgid "Link to a post"
375 msgstr "Бсылка Π½Π° сообщСниС"
375 msgstr "Бсылка Π½Π° сообщСниС"
376
376
377 #: templates/boards/staticpages/help.html:16
377 #: templates/boards/staticpages/help.html:16
378 msgid "Strikethrough text"
378 msgid "Strikethrough text"
379 msgstr "Π—Π°Ρ‡Π΅Ρ€ΠΊΠ½ΡƒΡ‚Ρ‹ΠΉ тСкст"
379 msgstr "Π—Π°Ρ‡Π΅Ρ€ΠΊΠ½ΡƒΡ‚Ρ‹ΠΉ тСкст"
380
380
381 #: templates/boards/staticpages/help.html:17
381 #: templates/boards/staticpages/help.html:17
382 msgid "You need to new line before:"
382 msgid "You need to new line before:"
383 msgstr "ΠŸΠ΅Ρ€Π΅Π΄ этими Ρ‚Π΅Π³Π°ΠΌΠΈ Π½ΡƒΠΆΠ½Π° новая строка:"
383 msgstr "ΠŸΠ΅Ρ€Π΅Π΄ этими Ρ‚Π΅Π³Π°ΠΌΠΈ Π½ΡƒΠΆΠ½Π° новая строка:"
384
384
385 #: templates/boards/staticpages/help.html:18
385 #: templates/boards/staticpages/help.html:18
386 msgid "Comment"
386 msgid "Comment"
387 msgstr "ΠšΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΉ"
387 msgstr "ΠšΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΉ"
388
388
389 #~ msgid "Archive"
389 #~ msgid "User ID"
390 #~ msgstr "Архив"
390 #~ msgstr "ID ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ"
@@ -1,192 +1,202 b''
1 # coding=utf-8
2
1 import markdown
3 import markdown
2 from markdown.inlinepatterns import Pattern, SubstituteTagPattern
4 from markdown.inlinepatterns import Pattern, SubstituteTagPattern
3 from markdown.util import etree
5 from markdown.util import etree
4 import boards
6 import boards
5
7
6 __author__ = 'neko259'
8 __author__ = 'neko259'
7
9
8
10
9 AUTOLINK_PATTERN = r'(https?://\S+)'
11 AUTOLINK_PATTERN = r'(https?://\S+)'
10 QUOTE_PATTERN = r'^(?<!>)(>[^>].*)$'
12 QUOTE_PATTERN = r'^(?<!>)(>[^>].*)$'
11 REFLINK_PATTERN = r'((>>)(\d+))'
13 REFLINK_PATTERN = r'((>>)(\d+))'
12 SPOILER_PATTERN = r'%%([^(%%)]+)%%'
14 SPOILER_PATTERN = r'%%([^(%%)]+)%%'
13 COMMENT_PATTERN = r'^(//(.+))'
15 COMMENT_PATTERN = r'^(//(.+))'
14 STRIKETHROUGH_PATTERN = r'~(.+)~'
16 STRIKETHROUGH_PATTERN = r'~(.+)~'
17 DASH_PATTERN = r'--'
15
18
16
19
17 class TextFormatter():
20 class TextFormatter():
18 """
21 """
19 An interface for formatter that can be used in the text format panel
22 An interface for formatter that can be used in the text format panel
20 """
23 """
21
24
22 name = ''
25 name = ''
23
26
24 # Left and right tags for the button preview
27 # Left and right tags for the button preview
25 preview_left = ''
28 preview_left = ''
26 preview_right = ''
29 preview_right = ''
27
30
28 # Left and right characters for the textarea input
31 # Left and right characters for the textarea input
29 format_left = ''
32 format_left = ''
30 format_right = ''
33 format_right = ''
31
34
32
35
33 class AutolinkPattern(Pattern):
36 class AutolinkPattern(Pattern):
34 def handleMatch(self, m):
37 def handleMatch(self, m):
35 link_element = etree.Element('a')
38 link_element = etree.Element('a')
36 href = m.group(2)
39 href = m.group(2)
37 link_element.set('href', href)
40 link_element.set('href', href)
38 link_element.text = href
41 link_element.text = href
39
42
40 return link_element
43 return link_element
41
44
42
45
43 class QuotePattern(Pattern, TextFormatter):
46 class QuotePattern(Pattern, TextFormatter):
44 name = ''
47 name = ''
45 preview_left = '<span class="quote">&gt; '
48 preview_left = '<span class="quote">&gt; '
46 preview_right = '</span>'
49 preview_right = '</span>'
47
50
48 format_left = '&gt;'
51 format_left = '&gt;'
49
52
50 def handleMatch(self, m):
53 def handleMatch(self, m):
51 quote_element = etree.Element('span')
54 quote_element = etree.Element('span')
52 quote_element.set('class', 'quote')
55 quote_element.set('class', 'quote')
53 quote_element.text = m.group(2)
56 quote_element.text = m.group(2)
54
57
55 return quote_element
58 return quote_element
56
59
57
60
58 class ReflinkPattern(Pattern):
61 class ReflinkPattern(Pattern):
59 def handleMatch(self, m):
62 def handleMatch(self, m):
60 post_id = m.group(4)
63 post_id = m.group(4)
61
64
62 posts = boards.models.Post.objects.filter(id=post_id)
65 posts = boards.models.Post.objects.filter(id=post_id)
63 if posts.count() > 0:
66 if posts.count() > 0:
64 ref_element = etree.Element('a')
67 ref_element = etree.Element('a')
65
68
66 post = posts[0]
69 post = posts[0]
67
70
68 ref_element.set('href', post.get_url())
71 ref_element.set('href', post.get_url())
69 ref_element.text = m.group(2)
72 ref_element.text = m.group(2)
70
73
71 return ref_element
74 return ref_element
72
75
73
76
74 class SpoilerPattern(Pattern, TextFormatter):
77 class SpoilerPattern(Pattern, TextFormatter):
75 name = 's'
78 name = 's'
76 preview_left = '<span class="spoiler">'
79 preview_left = '<span class="spoiler">'
77 preview_right = '</span>'
80 preview_right = '</span>'
78
81
79 format_left = '%%'
82 format_left = '%%'
80 format_right = '%%'
83 format_right = '%%'
81
84
82 def handleMatch(self, m):
85 def handleMatch(self, m):
83 quote_element = etree.Element('span')
86 quote_element = etree.Element('span')
84 quote_element.set('class', 'spoiler')
87 quote_element.set('class', 'spoiler')
85 quote_element.text = m.group(2)
88 quote_element.text = m.group(2)
86
89
87 return quote_element
90 return quote_element
88
91
89
92
90 class CommentPattern(Pattern, TextFormatter):
93 class CommentPattern(Pattern, TextFormatter):
91 name = ''
94 name = ''
92 preview_left = '<span class="comment">// '
95 preview_left = '<span class="comment">// '
93 preview_right = '</span>'
96 preview_right = '</span>'
94
97
95 format_left = '//'
98 format_left = '//'
96
99
97 def handleMatch(self, m):
100 def handleMatch(self, m):
98 quote_element = etree.Element('span')
101 quote_element = etree.Element('span')
99 quote_element.set('class', 'comment')
102 quote_element.set('class', 'comment')
100 quote_element.text = '//' + m.group(3)
103 quote_element.text = '//' + m.group(3)
101
104
102 return quote_element
105 return quote_element
103
106
104
107
105 class StrikeThroughPattern(Pattern, TextFormatter):
108 class StrikeThroughPattern(Pattern, TextFormatter):
106 name = 's'
109 name = 's'
107 preview_left = '<span class="strikethrough">'
110 preview_left = '<span class="strikethrough">'
108 preview_right = '</span>'
111 preview_right = '</span>'
109
112
110 format_left = '~'
113 format_left = '~'
111 format_right = '~'
114 format_right = '~'
112
115
113 def handleMatch(self, m):
116 def handleMatch(self, m):
114 quote_element = etree.Element('span')
117 quote_element = etree.Element('span')
115 quote_element.set('class', 'strikethrough')
118 quote_element.set('class', 'strikethrough')
116 quote_element.text = m.group(2)
119 quote_element.text = m.group(2)
117
120
118 return quote_element
121 return quote_element
119
122
120
123
121 class ItalicPattern(TextFormatter):
124 class ItalicPattern(TextFormatter):
122 name = 'i'
125 name = 'i'
123 preview_left = '<i>'
126 preview_left = '<i>'
124 preview_right = '</i>'
127 preview_right = '</i>'
125
128
126 format_left = '_'
129 format_left = '_'
127 format_right = '_'
130 format_right = '_'
128
131
129
132
130 class BoldPattern(TextFormatter):
133 class BoldPattern(TextFormatter):
131 name = 'b'
134 name = 'b'
132 preview_left = '<b>'
135 preview_left = '<b>'
133 preview_right = '</b>'
136 preview_right = '</b>'
134
137
135 format_left = '__'
138 format_left = '__'
136 format_right = '__'
139 format_right = '__'
137
140
138
141
139 class CodePattern(TextFormatter):
142 class CodePattern(TextFormatter):
140 name = 'code'
143 name = 'code'
141 preview_left = '<code>'
144 preview_left = '<code>'
142 preview_right = '</code>'
145 preview_right = '</code>'
143
146
144 format_left = ' '
147 format_left = ' '
145
148
146
149
150 class DashPattern(Pattern):
151 def handleMatch(self, m):
152 return u'β€”'
153
154
147 class NeboardMarkdown(markdown.Extension):
155 class NeboardMarkdown(markdown.Extension):
148 def extendMarkdown(self, md, md_globals):
156 def extendMarkdown(self, md, md_globals):
149 self._add_neboard_patterns(md)
157 self._add_neboard_patterns(md)
150 self._delete_patterns(md)
158 self._delete_patterns(md)
151
159
152 def _delete_patterns(self, md):
160 def _delete_patterns(self, md):
153 del md.parser.blockprocessors['quote']
161 del md.parser.blockprocessors['quote']
154
162
155 del md.inlinePatterns['image_link']
163 del md.inlinePatterns['image_link']
156 del md.inlinePatterns['image_reference']
164 del md.inlinePatterns['image_reference']
157
165
158 def _add_neboard_patterns(self, md):
166 def _add_neboard_patterns(self, md):
159 autolink = AutolinkPattern(AUTOLINK_PATTERN, md)
167 autolink = AutolinkPattern(AUTOLINK_PATTERN, md)
160 quote = QuotePattern(QUOTE_PATTERN, md)
168 quote = QuotePattern(QUOTE_PATTERN, md)
161 reflink = ReflinkPattern(REFLINK_PATTERN, md)
169 reflink = ReflinkPattern(REFLINK_PATTERN, md)
162 spoiler = SpoilerPattern(SPOILER_PATTERN, md)
170 spoiler = SpoilerPattern(SPOILER_PATTERN, md)
163 comment = CommentPattern(COMMENT_PATTERN, md)
171 comment = CommentPattern(COMMENT_PATTERN, md)
164 strikethrough = StrikeThroughPattern(STRIKETHROUGH_PATTERN, md)
172 strikethrough = StrikeThroughPattern(STRIKETHROUGH_PATTERN, md)
173 dash = DashPattern(DASH_PATTERN, md)
165
174
166 md.inlinePatterns[u'autolink_ext'] = autolink
175 md.inlinePatterns[u'autolink_ext'] = autolink
167 md.inlinePatterns[u'spoiler'] = spoiler
176 md.inlinePatterns[u'spoiler'] = spoiler
168 md.inlinePatterns[u'strikethrough'] = strikethrough
177 md.inlinePatterns[u'strikethrough'] = strikethrough
169 md.inlinePatterns[u'comment'] = comment
178 md.inlinePatterns[u'comment'] = comment
170 md.inlinePatterns[u'reflink'] = reflink
179 md.inlinePatterns[u'reflink'] = reflink
171 md.inlinePatterns[u'quote'] = quote
180 md.inlinePatterns[u'quote'] = quote
181 md.inlinePatterns[u'dash'] = dash
172
182
173
183
174 def make_extension(configs=None):
184 def make_extension(configs=None):
175 return NeboardMarkdown(configs=configs)
185 return NeboardMarkdown(configs=configs)
176
186
177 neboard_extension = make_extension()
187 neboard_extension = make_extension()
178
188
179
189
180 def markdown_extended(markup):
190 def markdown_extended(markup):
181 return markdown.markdown(markup, [neboard_extension, 'nl2br'],
191 return markdown.markdown(markup, [neboard_extension, 'nl2br'],
182 safe_mode='escape')
192 safe_mode='escape')
183
193
184 formatters = [
194 formatters = [
185 QuotePattern,
195 QuotePattern,
186 SpoilerPattern,
196 SpoilerPattern,
187 ItalicPattern,
197 ItalicPattern,
188 BoldPattern,
198 BoldPattern,
189 CommentPattern,
199 CommentPattern,
190 StrikeThroughPattern,
200 StrikeThroughPattern,
191 CodePattern,
201 CodePattern,
192 ]
202 ]
@@ -1,41 +1,42 b''
1 from django.shortcuts import redirect
1 from django.shortcuts import redirect
2 from boards import views, utils
2 from boards import utils
3 from boards.models import Ban
3 from boards.models import Ban
4 from django.utils.html import strip_spaces_between_tags
4 from django.utils.html import strip_spaces_between_tags
5 from django.conf import settings
5 from django.conf import settings
6 from boards.views.banned import BannedView
6
7
7 RESPONSE_CONTENT_TYPE = 'Content-Type'
8 RESPONSE_CONTENT_TYPE = 'Content-Type'
8
9
9 TYPE_HTML = 'text/html'
10 TYPE_HTML = 'text/html'
10
11
11
12
12 class BanMiddleware:
13 class BanMiddleware:
13 """
14 """
14 This is run before showing the thread. Banned users don't need to see
15 This is run before showing the thread. Banned users don't need to see
15 anything
16 anything
16 """
17 """
17
18
18 def process_view(self, request, view_func, view_args, view_kwargs):
19 def process_view(self, request, view_func, view_args, view_kwargs):
19
20
20 if view_func != views.banned.BannedView.as_view:
21 if view_func != BannedView.as_view:
21 ip = utils.get_client_ip(request)
22 ip = utils.get_client_ip(request)
22 bans = Ban.objects.filter(ip=ip)
23 bans = Ban.objects.filter(ip=ip)
23
24
24 if bans.exists():
25 if bans.exists():
25 ban = bans[0]
26 ban = bans[0]
26 if not ban.can_read:
27 if not ban.can_read:
27 return redirect('banned')
28 return redirect('banned')
28
29
29
30
30 class MinifyHTMLMiddleware(object):
31 class MinifyHTMLMiddleware(object):
31 def process_response(self, request, response):
32 def process_response(self, request, response):
32 try:
33 try:
33 compress_html = settings.COMPRESS_HTML
34 compress_html = settings.COMPRESS_HTML
34 except AttributeError:
35 except AttributeError:
35 compress_html = False
36 compress_html = False
36
37
37 if RESPONSE_CONTENT_TYPE in response\
38 if RESPONSE_CONTENT_TYPE in response\
38 and TYPE_HTML in response[RESPONSE_CONTENT_TYPE] and compress_html:
39 and TYPE_HTML in response[RESPONSE_CONTENT_TYPE] and compress_html:
39 response.content = strip_spaces_between_tags(
40 response.content = strip_spaces_between_tags(
40 response.content.strip())
41 response.content.strip())
41 return response No newline at end of file
42 return response
@@ -1,8 +1,9 b''
1 __author__ = 'neko259'
1 __author__ = 'neko259'
2
2
3 from boards.models.image import PostImage
4 from boards.models.thread import Thread
3 from boards.models.post import Post
5 from boards.models.post import Post
4 from boards.models.post import Thread
5 from boards.models.tag import Tag
6 from boards.models.tag import Tag
6 from boards.models.user import Ban
7 from boards.models.user import Ban
7 from boards.models.user import Setting
8 from boards.models.user import Setting
8 from boards.models.user import User
9 from boards.models.user import User
@@ -1,503 +1,381 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 os
4 import os
5 from random import random
5 from random import random
6 import time
6 import time
7 import re
7 import re
8 import hashlib
8 import hashlib
9
9
10 from django.core.cache import cache
10 from django.core.cache import cache
11 from django.core.urlresolvers import reverse
11 from django.core.urlresolvers import reverse
12 from django.db import models, transaction
12 from django.db import models, transaction
13 from django.template.loader import render_to_string
13 from django.utils import timezone
14 from django.utils import timezone
14 from markupfield.fields import MarkupField
15 from markupfield.fields import MarkupField
16 from boards.models import PostImage
17 from boards.models.base import Viewable
15
18
19 from boards.models.thread import Thread
16 from neboard import settings
20 from neboard import settings
17 from boards import thumbs
21 from boards import thumbs
18
22
19
23
20 APP_LABEL_BOARDS = 'boards'
24 APP_LABEL_BOARDS = 'boards'
21
25
22 CACHE_KEY_PPD = 'ppd'
26 CACHE_KEY_PPD = 'ppd'
23 CACHE_KEY_POST_URL = 'post_url'
27 CACHE_KEY_POST_URL = 'post_url'
24 CACHE_KEY_OPENING_POST = 'opening_post_id'
25
28
26 POSTS_PER_DAY_RANGE = range(7)
29 POSTS_PER_DAY_RANGE = range(7)
27
30
28 BAN_REASON_AUTO = 'Auto'
31 BAN_REASON_AUTO = 'Auto'
29
32
30 IMAGE_THUMB_SIZE = (200, 150)
33 IMAGE_THUMB_SIZE = (200, 150)
31
34
32 TITLE_MAX_LENGTH = 200
35 TITLE_MAX_LENGTH = 200
33
36
34 DEFAULT_MARKUP_TYPE = 'markdown'
37 DEFAULT_MARKUP_TYPE = 'markdown'
35
38
39 # TODO This should be removed when no code relies on it because thread id field
40 # was removed a long time ago
36 NO_PARENT = -1
41 NO_PARENT = -1
42
43 # TODO This should be removed
37 NO_IP = '0.0.0.0'
44 NO_IP = '0.0.0.0'
45
46 # TODO Real user agent should be saved instead of this
38 UNKNOWN_UA = ''
47 UNKNOWN_UA = ''
48
49 # TODO This should be checked for usage and removed because a nativa
50 # paginator is used now
39 ALL_PAGES = -1
51 ALL_PAGES = -1
52
40 IMAGES_DIRECTORY = 'images/'
53 IMAGES_DIRECTORY = 'images/'
41 FILE_EXTENSION_DELIMITER = '.'
54 FILE_EXTENSION_DELIMITER = '.'
42
55
43 SETTING_MODERATE = "moderate"
56 SETTING_MODERATE = "moderate"
44
57
45 REGEX_REPLY = re.compile('>>(\d+)')
58 REGEX_REPLY = re.compile('>>(\d+)')
46
59
47 logger = logging.getLogger(__name__)
60 logger = logging.getLogger(__name__)
48
61
49
62
50 class PostManager(models.Manager):
63 class PostManager(models.Manager):
51
64
52 def create_post(self, title, text, image=None, thread=None,
65 def create_post(self, title, text, image=None, thread=None,
53 ip=NO_IP, tags=None, user=None):
66 ip=NO_IP, tags=None, user=None):
54 """
67 """
55 Creates new post
68 Creates new post
56 """
69 """
57
70
58 posting_time = timezone.now()
71 posting_time = timezone.now()
59 if not thread:
72 if not thread:
60 thread = Thread.objects.create(bump_time=posting_time,
73 thread = Thread.objects.create(bump_time=posting_time,
61 last_edit_time=posting_time)
74 last_edit_time=posting_time)
62 new_thread = True
75 new_thread = True
63 else:
76 else:
64 thread.bump()
77 thread.bump()
65 thread.last_edit_time = posting_time
78 thread.last_edit_time = posting_time
66 thread.save()
79 thread.save()
67 new_thread = False
80 new_thread = False
68
81
69 post = self.create(title=title,
82 post = self.create(title=title,
70 text=text,
83 text=text,
71 pub_time=posting_time,
84 pub_time=posting_time,
72 thread_new=thread,
85 thread_new=thread,
73 image=image,
74 poster_ip=ip,
86 poster_ip=ip,
75 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
87 poster_user_agent=UNKNOWN_UA, # TODO Get UA at
76 # last!
88 # last!
77 last_edit_time=posting_time,
89 last_edit_time=posting_time,
78 user=user)
90 user=user)
79
91
92 if image:
93 post_image = PostImage.objects.create(image=image)
94 post.images.add(post_image)
95 logger.info('Created image #%d for post #%d' % (post_image.id,
96 post.id))
97
80 thread.replies.add(post)
98 thread.replies.add(post)
81 if tags:
99 if tags:
82 linked_tags = []
100 linked_tags = []
83 for tag in tags:
101 for tag in tags:
84 tag_linked_tags = tag.get_linked_tags()
102 tag_linked_tags = tag.get_linked_tags()
85 if len(tag_linked_tags) > 0:
103 if len(tag_linked_tags) > 0:
86 linked_tags.extend(tag_linked_tags)
104 linked_tags.extend(tag_linked_tags)
87
105
88 tags.extend(linked_tags)
106 tags.extend(linked_tags)
89 map(thread.add_tag, tags)
107 map(thread.add_tag, tags)
90
108
91 if new_thread:
109 if new_thread:
92 self._delete_old_threads()
110 self._delete_old_threads()
93 self.connect_replies(post)
111 self.connect_replies(post)
94
112
95 logger.info('Created post #%d' % post.id)
113 logger.info('Created post #%d' % post.id)
96
114
97 return post
115 return post
98
116
99 def delete_post(self, post):
117 def delete_post(self, post):
100 """
118 """
101 Deletes post and update or delete its thread
119 Deletes post and update or delete its thread
102 """
120 """
103
121
104 post_id = post.id
122 post_id = post.id
105
123
106 thread = post.get_thread()
124 thread = post.get_thread()
107
125
108 if post.is_opening():
126 if post.is_opening():
109 thread.delete_with_posts()
127 thread.delete_with_posts()
110 else:
128 else:
111 thread.last_edit_time = timezone.now()
129 thread.last_edit_time = timezone.now()
112 thread.save()
130 thread.save()
113
131
114 post.delete()
132 post.delete()
115
133
116 logger.info('Deleted post #%d' % post_id)
134 logger.info('Deleted post #%d' % post_id)
117
135
118 def delete_posts_by_ip(self, ip):
136 def delete_posts_by_ip(self, ip):
119 """
137 """
120 Deletes all posts of the author with same IP
138 Deletes all posts of the author with same IP
121 """
139 """
122
140
123 posts = self.filter(poster_ip=ip)
141 posts = self.filter(poster_ip=ip)
124 map(self.delete_post, posts)
142 map(self.delete_post, posts)
125
143
126 # TODO Move this method to thread manager
144 # TODO Move this method to thread manager
145 # TODO Rename it, because the threads are archived instead of plain
146 # removal. Split the delete and archive methods and make a setting to
147 # enable or disable archiving.
127 def _delete_old_threads(self):
148 def _delete_old_threads(self):
128 """
149 """
129 Preserves maximum thread count. If there are too many threads,
150 Preserves maximum thread count. If there are too many threads,
130 archive the old ones.
151 archive the old ones.
131 """
152 """
132
153
133 threads = Thread.objects.filter(archived=False).order_by('-bump_time')
154 threads = Thread.objects.filter(archived=False).order_by('-bump_time')
134 thread_count = threads.count()
155 thread_count = threads.count()
135
156
136 if thread_count > settings.MAX_THREAD_COUNT:
157 if thread_count > settings.MAX_THREAD_COUNT:
137 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
158 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
138 old_threads = threads[thread_count - num_threads_to_delete:]
159 old_threads = threads[thread_count - num_threads_to_delete:]
139
160
140 for thread in old_threads:
161 for thread in old_threads:
141 thread.archived = True
162 thread.archived = True
142 thread.last_edit_time = timezone.now()
163 thread.last_edit_time = timezone.now()
143 thread.save()
164 thread.save()
144
165
145 logger.info('Archived %d old threads' % num_threads_to_delete)
166 logger.info('Archived %d old threads' % num_threads_to_delete)
146
167
147 def connect_replies(self, post):
168 def connect_replies(self, post):
148 """
169 """
149 Connects replies to a post to show them as a reflink map
170 Connects replies to a post to show them as a reflink map
150 """
171 """
151
172
152 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
173 for reply_number in re.finditer(REGEX_REPLY, post.text.raw):
153 post_id = reply_number.group(1)
174 post_id = reply_number.group(1)
154 ref_post = self.filter(id=post_id)
175 ref_post = self.filter(id=post_id)
155 if ref_post.count() > 0:
176 if ref_post.count() > 0:
156 referenced_post = ref_post[0]
177 referenced_post = ref_post[0]
157 referenced_post.referenced_posts.add(post)
178 referenced_post.referenced_posts.add(post)
158 referenced_post.last_edit_time = post.pub_time
179 referenced_post.last_edit_time = post.pub_time
159 referenced_post.build_refmap()
180 referenced_post.build_refmap()
160 referenced_post.save()
181 referenced_post.save()
161
182
162 referenced_thread = referenced_post.get_thread()
183 referenced_thread = referenced_post.get_thread()
163 referenced_thread.last_edit_time = post.pub_time
184 referenced_thread.last_edit_time = post.pub_time
164 referenced_thread.save()
185 referenced_thread.save()
165
186
166 def get_posts_per_day(self):
187 def get_posts_per_day(self):
167 """
188 """
168 Gets average count of posts per day for the last 7 days
189 Gets average count of posts per day for the last 7 days
169 """
190 """
170
191
171 today = date.today()
192 today = date.today()
172 ppd = cache.get(CACHE_KEY_PPD + str(today))
193 ppd = cache.get(CACHE_KEY_PPD + str(today))
173 if ppd:
194 if ppd:
174 return ppd
195 return ppd
175
196
176 posts_per_days = []
197 posts_per_days = []
177 for i in POSTS_PER_DAY_RANGE:
198 for i in POSTS_PER_DAY_RANGE:
178 day_end = today - timedelta(i + 1)
199 day_end = today - timedelta(i + 1)
179 day_start = today - timedelta(i + 2)
200 day_start = today - timedelta(i + 2)
180
201
181 day_time_start = timezone.make_aware(datetime.combine(
202 day_time_start = timezone.make_aware(datetime.combine(
182 day_start, dtime()), timezone.get_current_timezone())
203 day_start, dtime()), timezone.get_current_timezone())
183 day_time_end = timezone.make_aware(datetime.combine(
204 day_time_end = timezone.make_aware(datetime.combine(
184 day_end, dtime()), timezone.get_current_timezone())
205 day_end, dtime()), timezone.get_current_timezone())
185
206
186 posts_per_days.append(float(self.filter(
207 posts_per_days.append(float(self.filter(
187 pub_time__lte=day_time_end,
208 pub_time__lte=day_time_end,
188 pub_time__gte=day_time_start).count()))
209 pub_time__gte=day_time_start).count()))
189
210
190 ppd = (sum(posts_per_day for posts_per_day in posts_per_days) /
211 ppd = (sum(posts_per_day for posts_per_day in posts_per_days) /
191 len(posts_per_days))
212 len(posts_per_days))
192 cache.set(CACHE_KEY_PPD + str(today), ppd)
213 cache.set(CACHE_KEY_PPD + str(today), ppd)
193 return ppd
214 return ppd
194
215
195
216
196 class Post(models.Model):
217 class Post(models.Model, Viewable):
197 """A post is a message."""
218 """A post is a message."""
198
219
199 objects = PostManager()
220 objects = PostManager()
200
221
201 class Meta:
222 class Meta:
202 app_label = APP_LABEL_BOARDS
223 app_label = APP_LABEL_BOARDS
203 ordering = ('id',)
224 ordering = ('id',)
204
225
205 # TODO Save original file name to some field
206 def _update_image_filename(self, filename):
207 """
208 Gets unique image filename
209 """
210
211 path = IMAGES_DIRECTORY
212 new_name = str(int(time.mktime(time.gmtime())))
213 new_name += str(int(random() * 1000))
214 new_name += FILE_EXTENSION_DELIMITER
215 new_name += filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
216
217 return os.path.join(path, new_name)
218
219 title = models.CharField(max_length=TITLE_MAX_LENGTH)
226 title = models.CharField(max_length=TITLE_MAX_LENGTH)
220 pub_time = models.DateTimeField()
227 pub_time = models.DateTimeField()
221 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
228 text = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
222 escape_html=False)
229 escape_html=False)
223
230
224 image_width = models.IntegerField(default=0)
231 images = models.ManyToManyField(PostImage, null=True, blank=True,
225 image_height = models.IntegerField(default=0)
232 related_name='ip+', db_index=True)
226
227 image_pre_width = models.IntegerField(default=0)
228 image_pre_height = models.IntegerField(default=0)
229
230 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
231 blank=True, sizes=(IMAGE_THUMB_SIZE,),
232 width_field='image_width',
233 height_field='image_height',
234 preview_width_field='image_pre_width',
235 preview_height_field='image_pre_height')
236 image_hash = models.CharField(max_length=36)
237
233
238 poster_ip = models.GenericIPAddressField()
234 poster_ip = models.GenericIPAddressField()
239 poster_user_agent = models.TextField()
235 poster_user_agent = models.TextField()
240
236
241 thread_new = models.ForeignKey('Thread', null=True, default=None,
237 thread_new = models.ForeignKey('Thread', null=True, default=None,
242 db_index=True)
238 db_index=True)
243 last_edit_time = models.DateTimeField()
239 last_edit_time = models.DateTimeField()
244 user = models.ForeignKey('User', null=True, default=None, db_index=True)
240 user = models.ForeignKey('User', null=True, default=None, db_index=True)
245
241
246 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
242 referenced_posts = models.ManyToManyField('Post', symmetrical=False,
247 null=True,
243 null=True,
248 blank=True, related_name='rfp+',
244 blank=True, related_name='rfp+',
249 db_index=True)
245 db_index=True)
250 refmap = models.TextField(null=True, blank=True)
246 refmap = models.TextField(null=True, blank=True)
251
247
252 def __unicode__(self):
248 def __unicode__(self):
253 return '#' + str(self.id) + ' ' + self.title + ' (' + \
249 return '#' + str(self.id) + ' ' + self.title + ' (' + \
254 self.text.raw[:50] + ')'
250 self.text.raw[:50] + ')'
255
251
256 def get_title(self):
252 def get_title(self):
257 """
253 """
258 Gets original post title or part of its text.
254 Gets original post title or part of its text.
259 """
255 """
260
256
261 title = self.title
257 title = self.title
262 if not title:
258 if not title:
263 title = self.text.rendered
259 title = self.text.rendered
264
260
265 return title
261 return title
266
262
267 def build_refmap(self):
263 def build_refmap(self):
268 map_string = ''
264 map_string = ''
269
265
270 first = True
266 first = True
271 for refpost in self.referenced_posts.all():
267 for refpost in self.referenced_posts.all():
272 if not first:
268 if not first:
273 map_string += ', '
269 map_string += ', '
274 map_string += '<a href="%s">&gt;&gt;%s</a>' % (refpost.get_url(), refpost.id)
270 map_string += '<a href="%s">&gt;&gt;%s</a>' % (refpost.get_url(), refpost.id)
275 first = False
271 first = False
276
272
277 self.refmap = map_string
273 self.refmap = map_string
278
274
279 def get_sorted_referenced_posts(self):
275 def get_sorted_referenced_posts(self):
280 return self.refmap
276 return self.refmap
281
277
282 def is_referenced(self):
278 def is_referenced(self):
283 return len(self.refmap) > 0
279 return len(self.refmap) > 0
284
280
285 def is_opening(self):
281 def is_opening(self):
286 """
282 """
287 Checks if this is an opening post or just a reply.
283 Checks if this is an opening post or just a reply.
288 """
284 """
289
285
290 return self.get_thread().get_opening_post_id() == self.id
286 return self.get_thread().get_opening_post_id() == self.id
291
287
292 def save(self, *args, **kwargs):
293 """
294 Saves the model and computes the image hash for deduplication purposes.
295 """
296
297 if not self.pk and self.image:
298 md5 = hashlib.md5()
299 for chunk in self.image.chunks():
300 md5.update(chunk)
301 self.image_hash = md5.hexdigest()
302 super(Post, self).save(*args, **kwargs)
303
304 @transaction.atomic
288 @transaction.atomic
305 def add_tag(self, tag):
289 def add_tag(self, tag):
306 edit_time = timezone.now()
290 edit_time = timezone.now()
307
291
308 thread = self.get_thread()
292 thread = self.get_thread()
309 thread.add_tag(tag)
293 thread.add_tag(tag)
310 self.last_edit_time = edit_time
294 self.last_edit_time = edit_time
311 self.save()
295 self.save()
312
296
313 thread.last_edit_time = edit_time
297 thread.last_edit_time = edit_time
314 thread.save()
298 thread.save()
315
299
316 @transaction.atomic
300 @transaction.atomic
317 def remove_tag(self, tag):
301 def remove_tag(self, tag):
318 edit_time = timezone.now()
302 edit_time = timezone.now()
319
303
320 thread = self.get_thread()
304 thread = self.get_thread()
321 thread.remove_tag(tag)
305 thread.remove_tag(tag)
322 self.last_edit_time = edit_time
306 self.last_edit_time = edit_time
323 self.save()
307 self.save()
324
308
325 thread.last_edit_time = edit_time
309 thread.last_edit_time = edit_time
326 thread.save()
310 thread.save()
327
311
328 def get_url(self, thread=None):
312 def get_url(self, thread=None):
329 """
313 """
330 Gets full url to the post.
314 Gets full url to the post.
331 """
315 """
332
316
333 cache_key = CACHE_KEY_POST_URL + str(self.id)
317 cache_key = CACHE_KEY_POST_URL + str(self.id)
334 link = cache.get(cache_key)
318 link = cache.get(cache_key)
335
319
336 if not link:
320 if not link:
337 if not thread:
321 if not thread:
338 thread = self.get_thread()
322 thread = self.get_thread()
339
323
340 opening_id = thread.get_opening_post_id()
324 opening_id = thread.get_opening_post_id()
341
325
342 if self.id != opening_id:
326 if self.id != opening_id:
343 link = reverse('thread', kwargs={
327 link = reverse('thread', kwargs={
344 'post_id': opening_id}) + '#' + str(self.id)
328 'post_id': opening_id}) + '#' + str(self.id)
345 else:
329 else:
346 link = reverse('thread', kwargs={'post_id': self.id})
330 link = reverse('thread', kwargs={'post_id': self.id})
347
331
348 cache.set(cache_key, link)
332 cache.set(cache_key, link)
349
333
350 return link
334 return link
351
335
352 def get_thread(self):
336 def get_thread(self):
353 """
337 """
354 Gets post's thread.
338 Gets post's thread.
355 """
339 """
356
340
357 return self.thread_new
341 return self.thread_new
358
342
359 def get_referenced_posts(self):
343 def get_referenced_posts(self):
360 return self.referenced_posts.only('id', 'thread_new')
344 return self.referenced_posts.only('id', 'thread_new')
361
345
362
346 def get_text(self):
363 class Thread(models.Model):
347 return self.text
364
365 class Meta:
366 app_label = APP_LABEL_BOARDS
367
368 tags = models.ManyToManyField('Tag')
369 bump_time = models.DateTimeField()
370 last_edit_time = models.DateTimeField()
371 replies = models.ManyToManyField('Post', symmetrical=False, null=True,
372 blank=True, related_name='tre+')
373 archived = models.BooleanField(default=False)
374
375 def get_tags(self):
376 """
377 Gets a sorted tag list.
378 """
379
380 return self.tags.order_by('name')
381
382 def bump(self):
383 """
384 Bumps (moves to up) thread if possible.
385 """
386
387 if self.can_bump():
388 self.bump_time = timezone.now()
389
390 logger.info('Bumped thread %d' % self.id)
391
392 def get_reply_count(self):
393 return self.replies.count()
394
348
395 def get_images_count(self):
349 def get_view(self, moderator=False, need_open_link=False,
396 return self.replies.filter(image_width__gt=0).count()
350 truncated=False, *args, **kwargs):
397
351 if 'is_opening' in kwargs:
398 def can_bump(self):
352 is_opening = kwargs['is_opening']
399 """
353 else:
400 Checks if the thread can be bumped by replying to it.
354 is_opening = self.is_opening()
401 """
402
403 if self.archived:
404 return False
405
406 post_count = self.get_reply_count()
407
408 return post_count < settings.MAX_POSTS_PER_THREAD
409
355
410 def delete_with_posts(self):
356 if 'thread' in kwargs:
411 """
357 thread = kwargs['thread']
412 Completely deletes thread and all its posts
358 else:
413 """
359 thread = self.get_thread()
414
415 if self.replies.exists():
416 self.replies.all().delete()
417
418 self.delete()
419
420 def get_last_replies(self):
421 """
422 Gets several last replies, not including opening post
423 """
424
425 if settings.LAST_REPLIES_COUNT > 0:
426 reply_count = self.get_reply_count()
427
360
428 if reply_count > 0:
361 if 'can_bump' in kwargs:
429 reply_count_to_show = min(settings.LAST_REPLIES_COUNT,
362 can_bump = kwargs['can_bump']
430 reply_count - 1)
363 else:
431 last_replies = self.replies.order_by(
364 can_bump = thread.can_bump()
432 'pub_time').defer('image_hash', 'poster_user_agent',
433 'text_markup_type')[
434 reply_count - reply_count_to_show:]
435
436 return last_replies
437
438 def get_skipped_replies_count(self):
439 """
440 Gets number of posts between opening post and last replies.
441 """
442 reply_count = self.get_reply_count()
443 last_replies_count = min(settings.LAST_REPLIES_COUNT,
444 reply_count - 1)
445 return reply_count - last_replies_count - 1
446
365
447 def get_replies(self, view_fields_only=False):
366 opening_post_id = thread.get_opening_post_id()
448 """
449 Gets sorted thread posts
450 """
451
452 query = self.replies.order_by('pub_time')
453 if view_fields_only:
454 query = query.defer(
455 'image_hash', 'poster_user_agent', 'text_markup_type')
456 return query.all()
457
458 def add_tag(self, tag):
459 """
460 Connects thread to a tag and tag to a thread
461 """
462
463 self.tags.add(tag)
464 tag.threads.add(self)
465
367
466 def remove_tag(self, tag):
368 return render_to_string('boards/post.html', {
467 self.tags.remove(tag)
369 'post': self,
468 tag.threads.remove(self)
370 'moderator': moderator,
469
371 'is_opening': is_opening,
470 def get_opening_post(self, only_id=False):
372 'thread': thread,
471 """
373 'bumpable': can_bump,
472 Gets the first post of the thread
374 'need_open_link': need_open_link,
473 """
375 'truncated': truncated,
474
376 'opening_post_id': opening_post_id,
475 query = self.replies.order_by('pub_time')
377 })
476 if only_id:
477 query = query.only('id')
478 opening_post = query.first()
479
480 return opening_post
481
378
482 def get_opening_post_id(self):
379 def get_first_image(self):
483 """
380 return self.images.earliest('id')
484 Gets ID of the first thread post.
485 """
486
487 cache_key = CACHE_KEY_OPENING_POST + str(self.id)
488 opening_post_id = cache.get(cache_key)
489 if not opening_post_id:
490 opening_post_id = self.get_opening_post(only_id=True).id
491 cache.set(cache_key, opening_post_id)
492
381
493 return opening_post_id
494
495 def __unicode__(self):
496 return str(self.id)
497
498 def get_pub_time(self):
499 """
500 Gets opening post's pub time because thread does not have its own one.
501 """
502
503 return self.get_opening_post().pub_time
@@ -1,128 +1,141 b''
1 from django.template.loader import render_to_string
1 from boards.models import Thread, Post
2 from boards.models import Thread, Post
2 from django.db import models
3 from django.db import models
3 from django.db.models import Count, Sum
4 from django.db.models import Count, Sum
5 from django.core.urlresolvers import reverse
6 from boards.models.base import Viewable
4
7
5 __author__ = 'neko259'
8 __author__ = 'neko259'
6
9
10 # TODO Tag popularity ratings are not used any more, remove all of this
7 MAX_TAG_FONT = 1
11 MAX_TAG_FONT = 1
8 MIN_TAG_FONT = 0.2
12 MIN_TAG_FONT = 0.2
9
13
10 TAG_POPULARITY_MULTIPLIER = 20
14 TAG_POPULARITY_MULTIPLIER = 20
11
15
12 ARCHIVE_POPULARITY_MODIFIER = 0.5
16 ARCHIVE_POPULARITY_MODIFIER = 0.5
13
17
14
18
15 class TagManager(models.Manager):
19 class TagManager(models.Manager):
16
20
17 def get_not_empty_tags(self):
21 def get_not_empty_tags(self):
18 """
22 """
19 Gets tags that have non-archived threads.
23 Gets tags that have non-archived threads.
20 """
24 """
21
25
22 tags = self.annotate(Count('threads')) \
26 tags = self.annotate(Count('threads')) \
23 .filter(threads__count__gt=0).filter(threads__archived=False) \
27 .filter(threads__count__gt=0).order_by('name')
24 .order_by('name')
25
28
26 return tags
29 return tags
27
30
28
31
29 class Tag(models.Model):
32 class Tag(models.Model, Viewable):
30 """
33 """
31 A tag is a text node assigned to the thread. The tag serves as a board
34 A tag is a text node assigned to the thread. The tag serves as a board
32 section. There can be multiple tags for each thread
35 section. There can be multiple tags for each thread
33 """
36 """
34
37
35 objects = TagManager()
38 objects = TagManager()
36
39
37 class Meta:
40 class Meta:
38 app_label = 'boards'
41 app_label = 'boards'
39 ordering = ('name',)
42 ordering = ('name',)
40
43
41 name = models.CharField(max_length=100, db_index=True)
44 name = models.CharField(max_length=100, db_index=True)
42 threads = models.ManyToManyField(Thread, null=True,
45 threads = models.ManyToManyField(Thread, null=True,
43 blank=True, related_name='tag+')
46 blank=True, related_name='tag+')
44 linked = models.ForeignKey('Tag', null=True, blank=True)
47 linked = models.ForeignKey('Tag', null=True, blank=True)
45
48
46 def __unicode__(self):
49 def __unicode__(self):
47 return self.name
50 return self.name
48
51
49 def is_empty(self):
52 def is_empty(self):
50 """
53 """
51 Checks if the tag has some threads.
54 Checks if the tag has some threads.
52 """
55 """
53
56
54 return self.get_thread_count() == 0
57 return self.get_thread_count() == 0
55
58
56 def get_thread_count(self):
59 def get_thread_count(self):
57 return self.threads.count()
60 return self.threads.count()
58
61
62 # TODO Remove, not used any more
59 def get_popularity(self):
63 def get_popularity(self):
60 """
64 """
61 Gets tag's popularity value as a percentage of overall board post
65 Gets tag's popularity value as a percentage of overall board post
62 count.
66 count.
63 """
67 """
64
68
65 all_post_count = Post.objects.count()
69 all_post_count = Post.objects.count()
66
70
67 tag_reply_count = 0.0
71 tag_reply_count = 0.0
68
72
69 tag_reply_count += self.get_post_count()
73 tag_reply_count += self.get_post_count()
70 tag_reply_count +=\
74 tag_reply_count +=\
71 self.get_post_count(archived=True) * ARCHIVE_POPULARITY_MODIFIER
75 self.get_post_count(archived=True) * ARCHIVE_POPULARITY_MODIFIER
72
76
73 popularity = tag_reply_count / all_post_count
77 popularity = tag_reply_count / all_post_count
74
78
75 return popularity
79 return popularity
76
80
77 def get_linked_tags(self):
81 def get_linked_tags(self):
78 """
82 """
79 Gets tags linked to the current one.
83 Gets tags linked to the current one.
80 """
84 """
81
85
82 tag_list = []
86 tag_list = []
83 self.get_linked_tags_list(tag_list)
87 self.get_linked_tags_list(tag_list)
84
88
85 return tag_list
89 return tag_list
86
90
87 def get_linked_tags_list(self, tag_list=[]):
91 def get_linked_tags_list(self, tag_list=[]):
88 """
92 """
89 Returns the list of tags linked to current. The list can be got
93 Returns the list of tags linked to current. The list can be got
90 through returned value or tag_list parameter
94 through returned value or tag_list parameter
91 """
95 """
92
96
93 linked_tag = self.linked
97 linked_tag = self.linked
94
98
95 if linked_tag and not (linked_tag in tag_list):
99 if linked_tag and not (linked_tag in tag_list):
96 tag_list.append(linked_tag)
100 tag_list.append(linked_tag)
97
101
98 linked_tag.get_linked_tags_list(tag_list)
102 linked_tag.get_linked_tags_list(tag_list)
99
103
104 # TODO Remove
100 def get_font_value(self):
105 def get_font_value(self):
101 """
106 """
102 Gets tag font value to differ most popular tags in the list
107 Gets tag font value to differ most popular tags in the list
103 """
108 """
104
109
105 popularity = self.get_popularity()
110 popularity = self.get_popularity()
106
111
107 font_value = popularity * Tag.objects.get_not_empty_tags().count()
112 font_value = popularity * Tag.objects.get_not_empty_tags().count()
108 font_value = max(font_value, MIN_TAG_FONT)
113 font_value = max(font_value, MIN_TAG_FONT)
109 font_value = min(font_value, MAX_TAG_FONT)
114 font_value = min(font_value, MAX_TAG_FONT)
110
115
111 return str(font_value)
116 return str(font_value)
112
117
113 def get_post_count(self, archived=False):
118 def get_post_count(self, archived=False):
114 """
119 """
115 Gets posts count for the tag's threads.
120 Gets posts count for the tag's threads.
116 """
121 """
117
122
118 posts_count = 0
123 posts_count = 0
119
124
120 threads = self.threads.filter(archived=archived)
125 threads = self.threads.filter(archived=archived)
121 if threads.exists():
126 if threads.exists():
122 posts_count = threads.annotate(posts_count=Count('replies')).aggregate(
127 posts_count = threads.annotate(posts_count=Count('replies')).aggregate(
123 posts_sum=Sum('posts_count'))['posts_sum']
128 posts_sum=Sum('posts_count'))['posts_sum']
124
129
125 if not posts_count:
130 if not posts_count:
126 posts_count = 0
131 posts_count = 0
127
132
128 return posts_count
133 return posts_count
134
135 def get_url(self):
136 return reverse('tag', kwargs={'tag_name': self.name})
137
138 def get_view(self, *args, **kwargs):
139 return render_to_string('boards/tag.html', {
140 'tag': self,
141 })
@@ -1,76 +1,81 b''
1 .ui-button {
1 .ui-button {
2 display: none;
2 display: none;
3 }
3 }
4
4
5 .ui-dialog-content {
5 .ui-dialog-content {
6 padding: 0;
6 padding: 0;
7 min-height: 0;
7 min-height: 0;
8 }
8 }
9
9
10 .mark_btn {
10 .mark_btn {
11 cursor: pointer;
11 cursor: pointer;
12 }
12 }
13
13
14 .img-full {
14 .img-full {
15 position: fixed;
15 position: fixed;
16 background-color: #CCC;
16 background-color: #CCC;
17 border: 1px solid #000;
17 border: 1px solid #000;
18 cursor: pointer;
18 cursor: pointer;
19 }
19 }
20
20
21 .strikethrough {
21 .strikethrough {
22 text-decoration: line-through;
22 text-decoration: line-through;
23 }
23 }
24
24
25 .post_preview {
25 .post_preview {
26 z-index: 300;
26 z-index: 300;
27 position:absolute;
27 position:absolute;
28 }
28 }
29
29
30 .gallery_image {
30 .gallery_image {
31 display: inline-block;
31 display: inline-block;
32 }
32 }
33
33
34 @media print {
34 @media print {
35 .post-form-w {
35 .post-form-w {
36 display: none;
36 display: none;
37 }
37 }
38 }
38 }
39
39
40 input[name="image"] {
40 input[name="image"] {
41 display: block;
41 display: block;
42 width: 100px;
42 width: 100px;
43 height: 100px;
43 height: 100px;
44 cursor: pointer;
44 cursor: pointer;
45 position: absolute;
45 position: absolute;
46 opacity: 0;
46 opacity: 0;
47 z-index: 1;
47 z-index: 1;
48 }
48 }
49
49
50 .file_wrap {
50 .file_wrap {
51 width: 100px;
51 width: 100px;
52 height: 100px;
52 height: 100px;
53 border: solid 1px white;
53 border: solid 1px white;
54 display: inline-block;
54 display: inline-block;
55 }
55 }
56
56
57 form > .file_wrap {
57 form > .file_wrap {
58 float: left;
58 float: left;
59 }
59 }
60
60
61 .file-thumb {
61 .file-thumb {
62 width: 100px;
62 width: 100px;
63 height: 100px;
63 height: 100px;
64 background-size: cover;
64 background-size: cover;
65 background-position: center;
65 background-position: center;
66 }
66 }
67
67
68 .compact-form-text {
68 .compact-form-text {
69 margin-left:110px;
69 margin-left:110px;
70 }
70 }
71
71
72 textarea, input {
72 textarea, input {
73 -moz-box-sizing: border-box;
73 -moz-box-sizing: border-box;
74 -webkit-box-sizing: border-box;
74 -webkit-box-sizing: border-box;
75 box-sizing: border-box;
75 box-sizing: border-box;
76 } No newline at end of file
76 }
77
78 .compact-form-text > textarea {
79 height: 100px;
80 width: 100%;
81 }
@@ -1,415 +1,421 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 {
16
17 }
18
19 .input_field_name {
20
21 }
22
23 .input_field_error {
15 .input_field_error {
24 color: #FF0000;
16 color: #FF0000;
25 }
17 }
26
18
27 .title {
19 .title {
28 font-weight: bold;
20 font-weight: bold;
29 color: #ffcc00;
21 color: #ffcc00;
30 }
22 }
31
23
32 .link, a {
24 .link, a {
33 color: #afdcec;
25 color: #afdcec;
34 }
26 }
35
27
36 .block {
28 .block {
37 display: inline-block;
29 display: inline-block;
38 vertical-align: top;
30 vertical-align: top;
39 }
31 }
40
32
41 .tag {
33 .tag {
42 /*color: #F5FFC9;*/
43 color: #FFD37D;
34 color: #FFD37D;
44 /*color: #b4cfec;*/
45 }
35 }
46
36
47 .post_id {
37 .post_id {
48 color: #fff380;
38 color: #fff380;
49 }
39 }
50
40
51 .post, .dead_post, .archive_post, #posts-table {
41 .post, .dead_post, .archive_post, #posts-table {
52 background: #333;
42 background: #333;
53 padding: 10px;
43 padding: 10px;
54 clear: left;
44 clear: left;
55 word-wrap: break-word;
45 word-wrap: break-word;
56 border-top: 1px solid #777;
46 border-top: 1px solid #777;
57 border-bottom: 1px solid #777;
47 border-bottom: 1px solid #777;
58 }
48 }
59
49
60 .post + .post {
50 .post + .post {
61 border-top: none;
51 border-top: none;
62 }
52 }
63
53
64 .dead_post + .dead_post {
54 .dead_post + .dead_post {
65 border-top: none;
55 border-top: none;
66 }
56 }
67
57
68 .archive_post + .archive_post {
58 .archive_post + .archive_post {
69 border-top: none;
59 border-top: none;
70 }
60 }
71
61
72 .metadata {
62 .metadata {
73 padding-top: 5px;
63 padding-top: 5px;
74 margin-top: 10px;
64 margin-top: 10px;
75 border-top: solid 1px #666;
65 border-top: solid 1px #666;
76 color: #ddd;
66 color: #ddd;
77 }
67 }
78
68
79 .navigation_panel, .tag_info {
69 .navigation_panel, .tag_info {
80 background: #444;
70 background: #444;
81 margin-bottom: 5px;
71 margin-bottom: 5px;
82 margin-top: 5px;
72 margin-top: 5px;
83 padding: 10px;
73 padding: 10px;
84 border-bottom: solid 1px #888;
74 border-bottom: solid 1px #888;
85 border-top: solid 1px #888;
75 border-top: solid 1px #888;
86 color: #eee;
76 color: #eee;
87 }
77 }
88
78
89 .navigation_panel .link {
79 .navigation_panel .link {
90 border-right: 1px solid #fff;
80 border-right: 1px solid #fff;
91 font-weight: bold;
81 font-weight: bold;
92 margin-right: 1ex;
82 margin-right: 1ex;
93 padding-right: 1ex;
83 padding-right: 1ex;
94 }
84 }
95 .navigation_panel .link:last-child {
85 .navigation_panel .link:last-child {
96 border-left: 1px solid #fff;
86 border-left: 1px solid #fff;
97 border-right: none;
87 border-right: none;
98 float: right;
88 float: right;
99 margin-left: 1ex;
89 margin-left: 1ex;
100 margin-right: 0;
90 margin-right: 0;
101 padding-left: 1ex;
91 padding-left: 1ex;
102 padding-right: 0;
92 padding-right: 0;
103 }
93 }
104
94
105 .navigation_panel::after, .post::after {
95 .navigation_panel::after, .post::after {
106 clear: both;
96 clear: both;
107 content: ".";
97 content: ".";
108 display: block;
98 display: block;
109 height: 0;
99 height: 0;
110 line-height: 0;
100 line-height: 0;
111 visibility: hidden;
101 visibility: hidden;
112 }
102 }
113
103
114 p {
104 p {
115 margin-top: .5em;
105 margin-top: .5em;
116 margin-bottom: .5em;
106 margin-bottom: .5em;
117 }
107 }
118
108
119 .post-form-w {
109 .post-form-w {
120 background: #333344;
110 background: #333344;
121 border-top: solid 1px #888;
111 border-top: solid 1px #888;
122 border-bottom: solid 1px #888;
112 border-bottom: solid 1px #888;
123 color: #fff;
113 color: #fff;
124 padding: 10px;
114 padding: 10px;
125 margin-bottom: 5px;
115 margin-bottom: 5px;
126 margin-top: 5px;
116 margin-top: 5px;
127 }
117 }
128
118
129 .form-row {
119 .form-row {
130 width: 100%;
120 width: 100%;
131 }
121 }
132
122
133 .form-label {
123 .form-label {
134 padding: .25em 1ex .25em 0;
124 padding: .25em 1ex .25em 0;
135 vertical-align: top;
125 vertical-align: top;
136 }
126 }
137
127
138 .form-input {
128 .form-input {
139 padding: .25em 0;
129 padding: .25em 0;
140 }
130 }
141
131
142 .form-errors {
132 .form-errors {
143 font-weight: bolder;
133 font-weight: bolder;
144 vertical-align: middle;
134 vertical-align: middle;
145 }
135 }
146
136
147 .post-form input:not([name="image"]), .post-form textarea {
137 .post-form input:not([name="image"]), .post-form textarea {
148 background: #333;
138 background: #333;
149 color: #fff;
139 color: #fff;
150 border: solid 1px;
140 border: solid 1px;
151 padding: 0;
141 padding: 0;
152 font: medium sans-serif;
142 font: medium sans-serif;
153 width: 100%;
143 width: 100%;
154 }
144 }
155
145
156 .form-submit {
146 .form-submit {
157 display: table;
147 display: table;
158 margin-bottom: 1ex;
148 margin-bottom: 1ex;
159 margin-top: 1ex;
149 margin-top: 1ex;
160 }
150 }
161
151
162 .form-title {
152 .form-title {
163 font-weight: bold;
153 font-weight: bold;
164 font-size: 2ex;
154 font-size: 2ex;
165 margin-bottom: 0.5ex;
155 margin-bottom: 0.5ex;
166 }
156 }
167
157
168 input[type="submit"] {
158 .post-form input[type="submit"], input[type="submit"] {
169 background: #222;
159 background: #222;
170 border: solid 2px #fff;
160 border: solid 2px #fff;
171 color: #fff;
161 color: #fff;
172 padding: 0.5ex;
162 padding: 0.5ex;
173 }
163 }
174
164
175 input[type="submit"]:hover {
165 input[type="submit"]:hover {
176 background: #060;
166 background: #060;
177 }
167 }
178
168
179 blockquote {
169 blockquote {
180 border-left: solid 2px;
170 border-left: solid 2px;
181 padding-left: 5px;
171 padding-left: 5px;
182 color: #B1FB17;
172 color: #B1FB17;
183 margin: 0;
173 margin: 0;
184 }
174 }
185
175
186 .post > .image {
176 .post > .image {
187 float: left;
177 float: left;
188 margin: 0 1ex .5ex 0;
178 margin: 0 1ex .5ex 0;
189 min-width: 1px;
179 min-width: 1px;
190 text-align: center;
180 text-align: center;
191 display: table-row;
181 display: table-row;
192 }
182 }
193
183
194 .post > .metadata {
184 .post > .metadata {
195 clear: left;
185 clear: left;
196 }
186 }
197
187
198 .get {
188 .get {
199 font-weight: bold;
189 font-weight: bold;
200 color: #d55;
190 color: #d55;
201 }
191 }
202
192
203 * {
193 * {
204 text-decoration: none;
194 text-decoration: none;
205 }
195 }
206
196
207 .dead_post {
197 .dead_post {
208 background-color: #442222;
198 background-color: #442222;
209 }
199 }
210
200
211 .archive_post {
201 .archive_post {
212 background-color: #000;
202 background-color: #000;
213 }
203 }
214
204
215 .mark_btn {
205 .mark_btn {
216 border: 1px solid;
206 border: 1px solid;
217 min-width: 2ex;
207 min-width: 2ex;
218 padding: 2px 2ex;
208 padding: 2px 2ex;
219 }
209 }
220
210
221 .mark_btn:hover {
211 .mark_btn:hover {
222 background: #555;
212 background: #555;
223 }
213 }
224
214
225 .quote {
215 .quote {
226 color: #92cf38;
216 color: #92cf38;
227 font-style: italic;
217 font-style: italic;
228 }
218 }
229
219
230 .spoiler {
220 .spoiler {
231 background: white;
221 background: white;
232 color: white;
222 color: white;
233 }
223 }
234
224
235 .spoiler:hover {
225 .spoiler:hover {
236 color: black;
226 color: black;
237 }
227 }
238
228
239 .comment {
229 .comment {
240 color: #eb2;
230 color: #eb2;
241 }
231 }
242
232
243 a:hover {
233 a:hover {
244 text-decoration: underline;
234 text-decoration: underline;
245 }
235 }
246
236
247 .last-replies {
237 .last-replies {
248 margin-left: 3ex;
238 margin-left: 3ex;
249 margin-right: 3ex;
239 margin-right: 3ex;
250 }
240 }
251
241
252 .thread {
242 .thread {
253 margin-bottom: 3ex;
243 margin-bottom: 3ex;
254 margin-top: 1ex;
244 margin-top: 1ex;
255 }
245 }
256
246
257 .post:target {
247 .post:target {
258 border: solid 2px white;
248 border: solid 2px white;
259 }
249 }
260
250
261 pre{
251 pre{
262 white-space:pre-wrap
252 white-space:pre-wrap
263 }
253 }
264
254
265 li {
255 li {
266 list-style-position: inside;
256 list-style-position: inside;
267 }
257 }
268
258
269 .fancybox-skin {
259 .fancybox-skin {
270 position: relative;
260 position: relative;
271 background-color: #fff;
261 background-color: #fff;
272 color: #ddd;
262 color: #ddd;
273 text-shadow: none;
263 text-shadow: none;
274 }
264 }
275
265
276 .fancybox-image {
266 .fancybox-image {
277 border: 1px solid black;
267 border: 1px solid black;
278 }
268 }
279
269
280 .image-mode-tab {
270 .image-mode-tab {
281 background: #444;
271 background: #444;
282 color: #eee;
272 color: #eee;
283 margin-top: 5px;
273 margin-top: 5px;
284 padding: 5px;
274 padding: 5px;
285 border-top: 1px solid #888;
275 border-top: 1px solid #888;
286 border-bottom: 1px solid #888;
276 border-bottom: 1px solid #888;
287 }
277 }
288
278
289 .image-mode-tab > label {
279 .image-mode-tab > label {
290 margin: 0 1ex;
280 margin: 0 1ex;
291 }
281 }
292
282
293 .image-mode-tab > label > input {
283 .image-mode-tab > label > input {
294 margin-right: .5ex;
284 margin-right: .5ex;
295 }
285 }
296
286
297 #posts-table {
287 #posts-table {
298 margin-top: 5px;
288 margin-top: 5px;
299 margin-bottom: 5px;
289 margin-bottom: 5px;
300 }
290 }
301
291
302 .tag_info > h2 {
292 .tag_info > h2 {
303 margin: 0;
293 margin: 0;
304 }
294 }
305
295
306 .post-info {
296 .post-info {
307 color: #ddd;
297 color: #ddd;
308 margin-bottom: 1ex;
298 margin-bottom: 1ex;
309 }
299 }
310
300
311 .moderator_info {
301 .moderator_info {
312 color: #e99d41;
302 color: #e99d41;
313 float: right;
303 float: right;
314 font-weight: bold;
304 font-weight: bold;
315 }
305 }
316
306
317 .refmap {
307 .refmap {
318 font-size: 0.9em;
308 font-size: 0.9em;
319 color: #ccc;
309 color: #ccc;
320 margin-top: 1em;
310 margin-top: 1em;
321 }
311 }
322
312
323 .fav {
313 .fav {
324 color: yellow;
314 color: yellow;
325 }
315 }
326
316
327 .not_fav {
317 .not_fav {
328 color: #ccc;
318 color: #ccc;
329 }
319 }
330
320
331 .role {
321 .role {
332 text-decoration: underline;
322 text-decoration: underline;
333 }
323 }
334
324
335 .form-email {
325 .form-email {
336 display: none;
326 display: none;
337 }
327 }
338
328
339 .footer {
329 .footer {
340 margin: 5px;
330 margin: 5px;
341 }
331 }
342
332
343 .bar-value {
333 .bar-value {
344 background: rgba(50, 55, 164, 0.45);
334 background: rgba(50, 55, 164, 0.45);
345 font-size: 0.9em;
335 font-size: 0.9em;
346 height: 1.5em;
336 height: 1.5em;
347 }
337 }
348
338
349 .bar-bg {
339 .bar-bg {
350 position: relative;
340 position: relative;
351 border-top: solid 1px #888;
341 border-top: solid 1px #888;
352 border-bottom: solid 1px #888;
342 border-bottom: solid 1px #888;
353 margin-top: 5px;
343 margin-top: 5px;
354 overflow: hidden;
344 overflow: hidden;
355 }
345 }
356
346
357 .bar-text {
347 .bar-text {
358 padding: 2px;
348 padding: 2px;
359 position: absolute;
349 position: absolute;
360 left: 0;
350 left: 0;
361 top: 0;
351 top: 0;
362 }
352 }
363
353
364 .page_link {
354 .page_link {
365 background: #444;
355 background: #444;
366 border-top: solid 1px #888;
356 border-top: solid 1px #888;
367 border-bottom: solid 1px #888;
357 border-bottom: solid 1px #888;
368 padding: 5px;
358 padding: 5px;
369 color: #eee;
359 color: #eee;
370 font-size: 2ex;
360 font-size: 2ex;
371 }
361 }
372
362
373 .skipped_replies {
363 .skipped_replies {
374 margin: 5px;
364 margin: 5px;
375 }
365 }
376
366
377 .current_page {
367 .current_page {
378 border: solid 1px #afdcec;
368 border: solid 1px #afdcec;
379 padding: 2px;
369 padding: 2px;
380 }
370 }
381
371
382 .current_mode {
372 .current_mode {
383 font-weight: bold;
373 font-weight: bold;
384 }
374 }
385
375
386 .gallery_image {
376 .gallery_image {
387 border: solid 1px;
377 border: solid 1px;
388 padding: 0.5ex;
378 padding: 0.5ex;
389 margin: 0.5ex;
379 margin: 0.5ex;
390 text-align: center;
380 text-align: center;
391 }
381 }
392
382
393 code {
383 code {
394 border: dashed 1px #ccc;
384 border: dashed 1px #ccc;
395 background: #111;
385 background: #111;
396 padding: 2px;
386 padding: 2px;
397 font-size: 1.2em;
387 font-size: 1.2em;
398 display: inline-block;
388 display: inline-block;
399 }
389 }
400
390
401 pre {
391 pre {
402 overflow: auto;
392 overflow: auto;
403 }
393 }
404
394
405 .img-full {
395 .img-full {
406 background: #222;
396 background: #222;
407 border: solid 1px white;
397 border: solid 1px white;
408 }
398 }
409
399
410 .tag_item {
400 .tag_item {
411 display: inline-block;
401 display: inline-block;
412 border: 1px dashed #666;
402 border: 1px dashed #666;
413 margin: 0.2ex;
403 margin: 0.2ex;
414 padding: 0.1ex;
404 padding: 0.1ex;
415 }
405 }
406
407 #id_models li {
408 list-style: none;
409 }
410
411 #id_q {
412 margin-left: 1ex;
413 }
414
415 ul {
416 padding-left: 0px;
417 }
418
419 .post_preview {
420 border: solid 2px white;
421 }
@@ -1,363 +1,371 b''
1 * {
1 * {
2 font-size: inherit;
2 font-size: inherit;
3 margin: 0;
3 margin: 0;
4 padding: 0;
4 padding: 0;
5 }
5 }
6 html {
6 html {
7 background: #fff;
7 background: #fff;
8 color: #000;
8 color: #000;
9 font: medium sans-serif;
9 font: medium sans-serif;
10 }
10 }
11 a {
11 a {
12 color: inherit;
12 color: inherit;
13 text-decoration: underline;
13 text-decoration: underline;
14 }
14 }
15 li {
15 li {
16 list-style-position: inside;
16 list-style-position: inside;
17 }
17 }
18
18
19 #admin_panel {
19 #admin_panel {
20 background: #182F6F;
20 background: #182F6F;
21 color: #fff;
21 color: #fff;
22 padding: .5ex 1ex .5ex 1ex;
22 padding: .5ex 1ex .5ex 1ex;
23 }
23 }
24
24
25 .navigation_panel {
25 .navigation_panel {
26 background: #182F6F;
26 background: #182F6F;
27 color: #B4CFEC;
27 color: #B4CFEC;
28 margin-bottom: 1em;
28 margin-bottom: 1em;
29 padding: .5ex 1ex 1ex 1ex;
29 padding: .5ex 1ex 1ex 1ex;
30 }
30 }
31 .navigation_panel::after {
31 .navigation_panel::after {
32 clear: both;
32 clear: both;
33 content: ".";
33 content: ".";
34 display: block;
34 display: block;
35 height: 0;
35 height: 0;
36 line-height: 0;
36 line-height: 0;
37 visibility: hidden;
37 visibility: hidden;
38 }
38 }
39
39
40 .navigation_panel a:link, .navigation_panel a:visited, .navigation_panel a:hover {
40 .navigation_panel a:link, .navigation_panel a:visited, .navigation_panel a:hover {
41 text-decoration: none;
41 text-decoration: none;
42 }
42 }
43
43
44 .navigation_panel .link {
44 .navigation_panel .link {
45 border-right: 1px solid #fff;
45 border-right: 1px solid #fff;
46 color: #fff;
46 color: #fff;
47 font-weight: bold;
47 font-weight: bold;
48 margin-right: 1ex;
48 margin-right: 1ex;
49 padding-right: 1ex;
49 padding-right: 1ex;
50 }
50 }
51 .navigation_panel .link:last-child {
51 .navigation_panel .link:last-child {
52 border-left: 1px solid #fff;
52 border-left: 1px solid #fff;
53 border-right: none;
53 border-right: none;
54 float: right;
54 float: right;
55 margin-left: 1ex;
55 margin-left: 1ex;
56 margin-right: 0;
56 margin-right: 0;
57 padding-left: 1ex;
57 padding-left: 1ex;
58 padding-right: 0;
58 padding-right: 0;
59 }
59 }
60
60
61 .navigation_panel .tag {
61 .navigation_panel .tag {
62 color: #fff;
62 color: #fff;
63 }
63 }
64
64
65 .input_field {
65 .input_field {
66
66
67 }
67 }
68
68
69 .input_field_name {
69 .input_field_name {
70
70
71 }
71 }
72
72
73 .input_field_error {
73 .input_field_error {
74 color: #FF0000;
74 color: #FF0000;
75 }
75 }
76
76
77
77
78 .title {
78 .title {
79 color: #182F6F;
79 color: #182F6F;
80 font-weight: bold;
80 font-weight: bold;
81 }
81 }
82
82
83 .post-form-w {
83 .post-form-w {
84 background: #182F6F;
84 background: #182F6F;
85 border-radius: 1ex;
85 border-radius: 1ex;
86 color: #fff;
86 color: #fff;
87 margin: 1em 1ex;
87 margin: 1em 1ex;
88 padding: 1ex;
88 padding: 1ex;
89 }
89 }
90
90
91 .form-row {
91 .form-row {
92 display: table;
92 display: table;
93 width: 100%;
93 width: 100%;
94 }
94 }
95 .form-label, .form-input {
95 .form-label, .form-input {
96 display: table-cell;
96 display: table-cell;
97 vertical-align: top;
97 vertical-align: top;
98 }
98 }
99 .form-label {
99 .form-label {
100 padding: .25em 1ex .25em 0;
100 padding: .25em 1ex .25em 0;
101 width: 14ex;
101 width: 14ex;
102 }
102 }
103 .form-input {
103 .form-input {
104 padding: .25em 0;
104 padding: .25em 0;
105 }
105 }
106 .form-input > * {
106 .form-input > * {
107 background: #fff;
107 background: #fff;
108 color: #000;
108 color: #000;
109 border: none;
109 border: none;
110 padding: 0;
110 padding: 0;
111 resize: vertical;
111 resize: vertical;
112 }
112 }
113
113
114 .form-input > :not(.file_wrap) {
114 .form-input > :not(.file_wrap) {
115 width: 100%;
115 width: 100%;
116 }
116 }
117
117
118 .form-submit {
118 .form-submit {
119 border-bottom: 1px solid #666;
119 border-bottom: 1px solid #666;
120 margin-bottom: .5em;
120 margin-bottom: .5em;
121 padding-bottom: .5em;
121 padding-bottom: .5em;
122 }
122 }
123 .form-title {
123 .form-title {
124 font-weight: bold;
124 font-weight: bold;
125 margin-bottom: .5em;
125 margin-bottom: .5em;
126 }
126 }
127 .post-form .settings_item {
127 .post-form .settings_item {
128 margin: .5em 0;
128 margin: .5em 0;
129 }
129 }
130 .form-submit input {
130 .form-submit input {
131 margin-top: .5em;
131 margin-top: .5em;
132 padding: .2em 1ex;
132 padding: .2em 1ex;
133 }
133 }
134 .form-label {
134 .form-label {
135 text-align: left;
135 text-align: left;
136 }
136 }
137
137
138 .block {
138 .block {
139 display: inline-block;
139 display: inline-block;
140 vertical-align: top;
140 vertical-align: top;
141 }
141 }
142
142
143 .post_id {
143 .post_id {
144 color: #a00;
144 color: #a00;
145 }
145 }
146
146
147 .post {
147 .post {
148 clear: left;
148 clear: left;
149 margin: 0 1ex 1em 1ex;
149 margin: 0 1ex 1em 1ex;
150 overflow-x: auto;
150 overflow-x: auto;
151 word-wrap: break-word;
151 word-wrap: break-word;
152 background: #FFF;
152 background: #FFF;
153 padding: 1ex;
153 padding: 1ex;
154 border: 1px solid #666;
154 border: 1px solid #666;
155 box-shadow: 1px 1px 2px 1px #666;
155 box-shadow: 1px 1px 2px 1px #666;
156 }
156 }
157
157
158 #posts > .post:last-child {
158 #posts > .post:last-child {
159 border-bottom: none;
159 border-bottom: none;
160 padding-bottom: 0;
160 padding-bottom: 0;
161 }
161 }
162
162
163 .metadata {
163 .metadata {
164 background: #C0E4E8;
164 background: #C0E4E8;
165 border: 1px solid #7F9699;
165 border: 1px solid #7F9699;
166 border-radius: .4ex;
166 border-radius: .4ex;
167 display: table;
167 display: table;
168 margin-top: .5em;
168 margin-top: .5em;
169 padding: .4em;
169 padding: .4em;
170 }
170 }
171
171
172 .post ul, .post ol {
172 .post ul, .post ol {
173 margin: .5em 0 .5em 3ex;
173 margin: .5em 0 .5em 3ex;
174 }
174 }
175 .post li {
175 .post li {
176 margin: .2em 0;
176 margin: .2em 0;
177 }
177 }
178 .post p {
178 .post p {
179 margin: .5em 0;
179 margin: .5em 0;
180 }
180 }
181 .post blockquote {
181 .post blockquote {
182 border-left: 3px solid #182F6F;
182 border-left: 3px solid #182F6F;
183 margin: .5em 0 .5em 3ex;
183 margin: .5em 0 .5em 3ex;
184 padding-left: 1ex;
184 padding-left: 1ex;
185 }
185 }
186 .post blockquote > blockquote {
186 .post blockquote > blockquote {
187 padding-top: .1em;
187 padding-top: .1em;
188 }
188 }
189
189
190 .post > .image {
190 .post > .image {
191 float: left;
191 float: left;
192 margin-right: 1ex;
192 margin-right: 1ex;
193 }
193 }
194 .post > .metadata {
194 .post > .metadata {
195 clear: left;
195 clear: left;
196 }
196 }
197
197
198 .post > .message .get {
198 .post > .message .get {
199 color: #182F6F; font-weight: bold;
199 color: #182F6F; font-weight: bold;
200 }
200 }
201
201
202 .dead_post > .metadata {
202 .dead_post > .metadata {
203 background: #eee;
203 background: #eee;
204 }
204 }
205
205
206 .quote {
206 .quote {
207 color: #182F6F;
207 color: #182F6F;
208 }
208 }
209
209
210 .spoiler {
210 .spoiler {
211 background: black;
211 background: black;
212 color: black;
212 color: black;
213 }
213 }
214
214
215 .spoiler:hover {
215 .spoiler:hover {
216 background: #ffffff;
216 background: #ffffff;
217 }
217 }
218
218
219 .comment {
219 .comment {
220 color: #557055;
220 color: #557055;
221 }
221 }
222
222
223 .last-replies {
223 .last-replies {
224 margin-left: 6ex;
224 margin-left: 6ex;
225 }
225 }
226
226
227 .thread > .post > .message > .post-info {
227 .thread > .post > .message > .post-info {
228 border-bottom: 1px solid #ccc;
228 border-bottom: 1px solid #ccc;
229 padding-bottom: .5em;
229 padding-bottom: .5em;
230 }
230 }
231
231
232 :target .post_id {
232 :target .post_id {
233 background: #182F6F;
233 background: #182F6F;
234 color: #FFF;
234 color: #FFF;
235 text-decoration: none;
235 text-decoration: none;
236 }
236 }
237
237
238 .image-mode-tab {
238 .image-mode-tab {
239 background: #182F6F;
239 background: #182F6F;
240 color: #FFF;
240 color: #FFF;
241 display: table;
241 display: table;
242 margin: 1em auto 1em 0;
242 margin: 1em auto 1em 0;
243 padding: .2em .5ex;
243 padding: .2em .5ex;
244 }
244 }
245
245
246 .image-mode-tab > label {
246 .image-mode-tab > label {
247 margin: 0 1ex;
247 margin: 0 1ex;
248 }
248 }
249
249
250 .image-mode-tab > label > input {
250 .image-mode-tab > label > input {
251 margin-right: .5ex;
251 margin-right: .5ex;
252 }
252 }
253
253
254 .tag_info, .page_link {
254 .tag_info, .page_link {
255 margin: 1em 0;
255 margin: 1em 0;
256 text-align: center;
256 text-align: center;
257 }
257 }
258
258
259 .form-errors {
259 .form-errors {
260 margin-left: 1ex;
260 margin-left: 1ex;
261 }
261 }
262
262
263 .moderator_info {
263 .moderator_info {
264 font-weight: bold;
264 font-weight: bold;
265 float: right;
265 float: right;
266 }
266 }
267
267
268 .refmap {
268 .refmap {
269 border: 1px dashed #aaa;
269 border: 1px dashed #aaa;
270 padding: 0.5em;
270 padding: 0.5em;
271 display: table;
271 display: table;
272 }
272 }
273
273
274 .fav {
274 .fav {
275 color: blue;
275 color: blue;
276 }
276 }
277
277
278 .not_fav {
278 .not_fav {
279 color: #ccc;
279 color: #ccc;
280 }
280 }
281
281
282 .role {
282 .role {
283 text-decoration: underline;
283 text-decoration: underline;
284 }
284 }
285
285
286 .form-email {
286 .form-email {
287 display: none;
287 display: none;
288 }
288 }
289
289
290 .bar-value {
290 .bar-value {
291 background: #E3E7F2;
291 background: #E3E7F2;
292 padding: .1em 1ex;
292 padding: .1em 1ex;
293 moz-box-sizing: border-box;
293 moz-box-sizing: border-box;
294 box-sizing: border-box;
294 box-sizing: border-box;
295 height: 1.5em;
295 height: 1.5em;
296 }
296 }
297
297
298 .bar-bg {
298 .bar-bg {
299 background: #EA4649;
299 background: #EA4649;
300 border: 1px solid #666;
300 border: 1px solid #666;
301 margin: 0 1ex 1em 1ex;
301 margin: 0 1ex 1em 1ex;
302 position: relative;
302 position: relative;
303 overflow: hidden;
303 overflow: hidden;
304 }
304 }
305
305
306 .bar-text {
306 .bar-text {
307 padding: 2px;
307 padding: 2px;
308 position: absolute;
308 position: absolute;
309 left: 0;
309 left: 0;
310 top: 0;
310 top: 0;
311 }
311 }
312
312
313 .skipped_replies {
313 .skipped_replies {
314 margin: 1ex;
314 margin: 1ex;
315 }
315 }
316
316
317 #mark-panel {
317 #mark-panel {
318 background: #eee;
318 background: #eee;
319 border-bottom: 1px solid #182F6F;
319 border-bottom: 1px solid #182F6F;
320 }
320 }
321
321
322 .mark_btn {
322 .mark_btn {
323 display: inline-block;
323 display: inline-block;
324 padding: .2em 1ex;
324 padding: .2em 1ex;
325 border-left: 1px solid #182F6F;
325 border-left: 1px solid #182F6F;
326 }
326 }
327
327
328 .mark_btn:first-child {
328 .mark_btn:first-child {
329 border-left: none;
329 border-left: none;
330 }
330 }
331
331
332 .mark_btn:last-child {
332 .mark_btn:last-child {
333 border-right: 1px solid #182F6F;
333 border-right: 1px solid #182F6F;
334 }
334 }
335
335
336 .current_page {
336 .current_page {
337 border-bottom: 1px solid #FFF;
337 border-bottom: 1px solid #FFF;
338 padding: 0px 0.5ex;
338 padding: 0px 0.5ex;
339 }
339 }
340
340
341 .image-mode-tab a {
341 .image-mode-tab a {
342 text-decoration: none;
342 text-decoration: none;
343 }
343 }
344 .image-mode-tab .current_mode::before {
344 .image-mode-tab .current_mode::before {
345 content: "βœ“ ";
345 content: "βœ“ ";
346 padding: 0 0 0 .5ex;
346 padding: 0 0 0 .5ex;
347 color: #182F6F;
347 color: #182F6F;
348 background: #FFF;
348 background: #FFF;
349 }
349 }
350 .image-mode-tab .current_mode {
350 .image-mode-tab .current_mode {
351 padding: 0 .5ex 0 0;
351 padding: 0 .5ex 0 0;
352 color: #182F6F;
352 color: #182F6F;
353 background: #FFF;
353 background: #FFF;
354 }
354 }
355
355
356 .gallery_image_metadata {
356 .gallery_image_metadata {
357 margin-bottom: 1em;
357 margin-bottom: 1em;
358 }
358 }
359
359
360 .swappable-form-full > form {
360 .swappable-form-full > form {
361 display: table;
361 display: table;
362 width: 100%;
362 width: 100%;
363 } No newline at end of file
363 }
364
365 #id_models li {
366 list-style: none;
367 }
368
369 #id_q {
370 margin-left: 1ex;
371 }
@@ -1,45 +1,41 b''
1 var isCompact = true;
1 var isCompact = false;
2
2
3 $('input[name=image]').wrap($('<div class="file_wrap"></div>'));
3 $('input[name=image]').wrap($('<div class="file_wrap"></div>'));
4
4
5 $('body').on('change', 'input[name=image]', function(event) {
5 $('body').on('change', 'input[name=image]', function(event) {
6 var file = event.target.files[0];
6 var file = event.target.files[0];
7
7
8 if(file.type.match('image.*')) {
8 if(file.type.match('image.*')) {
9 var fileReader = new FileReader();
9 var fileReader = new FileReader();
10
10
11 fileReader.addEventListener("load", function(event) {
11 fileReader.addEventListener("load", function(event) {
12 var wrapper = $('.file_wrap');
12 var wrapper = $('.file_wrap');
13
13
14 wrapper.find('.file-thumb').remove();
14 wrapper.find('.file-thumb').remove();
15 wrapper.append(
15 wrapper.append(
16 $('<div class="file-thumb" style="background-image: url('+event.target.result+')"></div>')
16 $('<div class="file-thumb" style="background-image: url('+event.target.result+')"></div>')
17 );
17 );
18 });
18 });
19
19
20 fileReader.readAsDataURL(file);
20 fileReader.readAsDataURL(file);
21 }
21 }
22 });
22 });
23
23
24 var compactForm = $('.swappable-form-compact');
25 var fullForm = $('.swappable-form-full');
24 var fullForm = $('.swappable-form-full');
26
25
27 function swapForm() {
26 function swapForm() {
28 compactForm.toggle();
27 if (isCompact) {
29 fullForm.toggle();
28 // TODO Use IDs (change the django form code) instead of absolute numbers
29 fullForm.find('textarea').appendTo(fullForm.find('.form-row')[4]);
30 fullForm.find('.file_wrap').appendTo(fullForm.find('.form-row')[7]);
31 fullForm.find('.form-row').show();
30
32
31 if (isCompact) {
33 scrollToBottom();
32 var oldText = compactForm.find('textarea')[0].value;
33 fullForm.find('textarea')[0].value = oldText;
34 } else {
34 } else {
35 var oldText = fullForm.find('textarea')[0].value;
35 fullForm.find('textarea').appendTo($('.compact-form-text'));
36 compactForm.find('textarea')[0].value = oldText;
36 fullForm.find('.file_wrap').insertBefore($('.compact-form-text'));
37 fullForm.find('.form-row').hide();
38 fullForm.find('input[type=text]').val('');
37 }
39 }
38 isCompact = !isCompact;
40 isCompact = !isCompact;
39
40 scrollToBottom();
41 }
41 }
42
43 if (compactForm.length > 0) {
44 fullForm.toggle();
45 }
@@ -1,45 +1,56 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 /**
26 /**
27 * An email is a hidden file to prevent spam bots from posting. It has to be
27 * An email is a hidden file to prevent spam bots from posting. It has to be
28 * hidden.
28 * hidden.
29 */
29 */
30 function hideEmailFromForm() {
30 function hideEmailFromForm() {
31 $('.form-email').parent().parent().hide();
31 $('.form-email').parent().parent().hide();
32 }
32 }
33
33
34 /**
35 * Highlight code blocks with code highlighter
36 */
37 function highlightCode() {
38 $('pre code').each(function(i, e) {
39 hljs.highlightBlock(e);
40 });
41 }
42
34 $( document ).ready(function() {
43 $( document ).ready(function() {
35 hideEmailFromForm();
44 hideEmailFromForm();
36
45
37 $("a[href='#top']").click(function() {
46 $("a[href='#top']").click(function() {
38 $("html, body").animate({ scrollTop: 0 }, "slow");
47 $("html, body").animate({ scrollTop: 0 }, "slow");
39 return false;
48 return false;
40 });
49 });
41
50
42 addImgPreview();
51 addImgPreview();
43
52
44 addRefLinkPreview();
53 addRefLinkPreview();
54
55 highlightCode();
45 });
56 });
@@ -1,59 +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 = '>>' + postId + '\n\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 += '> ' + selection + '\n\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 $('#full-form').toggle();
59 $(document).ready(function() {
60 swapForm();
61 })
@@ -1,264 +1,266 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 var THREAD_UPDATE_DELAY = 10000;
26 var THREAD_UPDATE_DELAY = 10000;
27
27
28 var loading = false;
28 var loading = false;
29 var lastUpdateTime = null;
29 var lastUpdateTime = null;
30 var unreadPosts = 0
30 var unreadPosts = 0
31
31
32 function blink(node) {
32 function blink(node) {
33 var blinkCount = 2;
33 var blinkCount = 2;
34
34
35 var nodeToAnimate = node;
35 var nodeToAnimate = node;
36 for (var i = 0; i < blinkCount; i++) {
36 for (var i = 0; i < blinkCount; i++) {
37 nodeToAnimate = nodeToAnimate.fadeTo('fast', 0.5).fadeTo('fast', 1.0);
37 nodeToAnimate = nodeToAnimate.fadeTo('fast', 0.5).fadeTo('fast', 1.0);
38 }
38 }
39 }
39 }
40
40
41 function updateThread() {
41 function updateThread() {
42 if (loading) {
42 if (loading) {
43 return;
43 return;
44 }
44 }
45
45
46 loading = true;
46 loading = true;
47
47
48 var threadPosts = $('div.thread').children('.post');
48 var threadPosts = $('div.thread').children('.post');
49
49
50 var lastPost = threadPosts.last();
50 var lastPost = threadPosts.last();
51 var threadId = threadPosts.first().attr('id');
51 var threadId = threadPosts.first().attr('id');
52
52
53 var diffUrl = '/api/diff_thread/' + threadId + '/' + lastUpdateTime + '/';
53 var diffUrl = '/api/diff_thread/' + threadId + '/' + lastUpdateTime + '/';
54 $.getJSON(diffUrl)
54 $.getJSON(diffUrl)
55 .success(function(data) {
55 .success(function(data) {
56 var bottom = isPageBottom();
56 var bottom = isPageBottom();
57
57
58 var lastUpdate = '';
58 var lastUpdate = '';
59
59
60 var addedPosts = data.added;
60 var addedPosts = data.added;
61 for (var i = 0; i < addedPosts.length; i++) {
61 for (var i = 0; i < addedPosts.length; i++) {
62 var postText = addedPosts[i];
62 var postText = addedPosts[i];
63
63
64 var post = $(postText);
64 var post = $(postText);
65
65
66 if (lastUpdate === '') {
66 if (lastUpdate === '') {
67 lastUpdate = post.find('.pub_time').text();
67 lastUpdate = post.find('.pub_time').text();
68 }
68 }
69
69
70 post.appendTo(lastPost.parent());
70 post.appendTo(lastPost.parent());
71 addRefLinkPreview(post[0]);
71 addRefLinkPreview(post[0]);
72 highlightCode();
72
73
73 lastPost = post;
74 lastPost = post;
74 blink(post);
75 blink(post);
75 }
76 }
76
77
77 var updatedPosts = data.updated;
78 var updatedPosts = data.updated;
78 for (var i = 0; i < updatedPosts.length; i++) {
79 for (var i = 0; i < updatedPosts.length; i++) {
79 var postText = updatedPosts[i];
80 var postText = updatedPosts[i];
80
81
81 var post = $(postText);
82 var post = $(postText);
82
83
83 if (lastUpdate === '') {
84 if (lastUpdate === '') {
84 lastUpdate = post.find('.pub_time').text();
85 lastUpdate = post.find('.pub_time').text();
85 }
86 }
86
87
87 var postId = post.attr('id');
88 var postId = post.attr('id');
88
89
89 var oldPost = $('div.thread').children('.post[id=' + postId + ']');
90 var oldPost = $('div.thread').children('.post[id=' + postId + ']');
90
91
91 oldPost.replaceWith(post);
92 oldPost.replaceWith(post);
92 addRefLinkPreview(post[0]);
93 addRefLinkPreview(post[0]);
94 highlightCode();
93
95
94 blink(post);
96 blink(post);
95 }
97 }
96
98
97 // TODO Process deleted posts
99 // TODO Process deleted posts
98
100
99 lastUpdateTime = data.last_update;
101 lastUpdateTime = data.last_update;
100 loading = false;
102 loading = false;
101
103
102 if (bottom) {
104 if (bottom) {
103 scrollToBottom();
105 scrollToBottom();
104 }
106 }
105
107
106 var hasPostChanges = (updatedPosts.length > 0)
108 var hasPostChanges = (updatedPosts.length > 0)
107 || (addedPosts.length > 0);
109 || (addedPosts.length > 0);
108 if (hasPostChanges) {
110 if (hasPostChanges) {
109 updateMetadataPanel(lastUpdate);
111 updateMetadataPanel(lastUpdate);
110 }
112 }
111
113
112 updateBumplimitProgress(data.added.length);
114 updateBumplimitProgress(data.added.length);
113
115
114 if (data.added.length + data.updated.length > 0) {
116 if (data.added.length + data.updated.length > 0) {
115 showNewPostsTitle(data.added.length);
117 showNewPostsTitle(data.added.length);
116 }
118 }
117 })
119 })
118 .error(function(data) {
120 .error(function(data) {
119 // TODO Show error message that server is unavailable?
121 // TODO Show error message that server is unavailable?
120
122
121 loading = false;
123 loading = false;
122 });
124 });
123 }
125 }
124
126
125 function isPageBottom() {
127 function isPageBottom() {
126 var scroll = $(window).scrollTop() / ($(document).height()
128 var scroll = $(window).scrollTop() / ($(document).height()
127 - $(window).height())
129 - $(window).height())
128
130
129 return scroll == 1
131 return scroll == 1
130 }
132 }
131
133
132 function initAutoupdate() {
134 function initAutoupdate() {
133 loading = false;
135 loading = false;
134
136
135 lastUpdateTime = $('.metapanel').attr('data-last-update');
137 lastUpdateTime = $('.metapanel').attr('data-last-update');
136
138
137 setInterval(updateThread, THREAD_UPDATE_DELAY);
139 setInterval(updateThread, THREAD_UPDATE_DELAY);
138 }
140 }
139
141
140 function getReplyCount() {
142 function getReplyCount() {
141 return $('.thread').children('.post').length
143 return $('.thread').children('.post').length
142 }
144 }
143
145
144 function getImageCount() {
146 function getImageCount() {
145 return $('.thread').find('img').length
147 return $('.thread').find('img').length
146 }
148 }
147
149
148 function updateMetadataPanel(lastUpdate) {
150 function updateMetadataPanel(lastUpdate) {
149 var replyCountField = $('#reply-count');
151 var replyCountField = $('#reply-count');
150 var imageCountField = $('#image-count');
152 var imageCountField = $('#image-count');
151
153
152 replyCountField.text(getReplyCount());
154 replyCountField.text(getReplyCount());
153 imageCountField.text(getImageCount());
155 imageCountField.text(getImageCount());
154
156
155 if (lastUpdate !== '') {
157 if (lastUpdate !== '') {
156 var lastUpdateField = $('#last-update');
158 var lastUpdateField = $('#last-update');
157 lastUpdateField.text(lastUpdate);
159 lastUpdateField.text(lastUpdate);
158 blink(lastUpdateField);
160 blink(lastUpdateField);
159 }
161 }
160
162
161 blink(replyCountField);
163 blink(replyCountField);
162 blink(imageCountField);
164 blink(imageCountField);
163 }
165 }
164
166
165 /**
167 /**
166 * Update bumplimit progress bar
168 * Update bumplimit progress bar
167 */
169 */
168 function updateBumplimitProgress(postDelta) {
170 function updateBumplimitProgress(postDelta) {
169 var progressBar = $('#bumplimit_progress');
171 var progressBar = $('#bumplimit_progress');
170 if (progressBar) {
172 if (progressBar) {
171 var postsToLimitElement = $('#left_to_limit');
173 var postsToLimitElement = $('#left_to_limit');
172
174
173 var oldPostsToLimit = parseInt(postsToLimitElement.text());
175 var oldPostsToLimit = parseInt(postsToLimitElement.text());
174 var postCount = getReplyCount();
176 var postCount = getReplyCount();
175 var bumplimit = postCount - postDelta + oldPostsToLimit;
177 var bumplimit = postCount - postDelta + oldPostsToLimit;
176
178
177 var newPostsToLimit = bumplimit - postCount;
179 var newPostsToLimit = bumplimit - postCount;
178 if (newPostsToLimit <= 0) {
180 if (newPostsToLimit <= 0) {
179 $('.bar-bg').remove();
181 $('.bar-bg').remove();
180 $('.thread').children('.post').addClass('dead_post');
182 $('.thread').children('.post').addClass('dead_post');
181 } else {
183 } else {
182 postsToLimitElement.text(newPostsToLimit);
184 postsToLimitElement.text(newPostsToLimit);
183 progressBar.width((100 - postCount / bumplimit * 100.0) + '%');
185 progressBar.width((100 - postCount / bumplimit * 100.0) + '%');
184 }
186 }
185 }
187 }
186 }
188 }
187
189
188 var documentOriginalTitle = '';
190 var documentOriginalTitle = '';
189 /**
191 /**
190 * Show 'new posts' text in the title if the document is not visible to a user
192 * Show 'new posts' text in the title if the document is not visible to a user
191 */
193 */
192 function showNewPostsTitle(newPostCount) {
194 function showNewPostsTitle(newPostCount) {
193 if (document.hidden) {
195 if (document.hidden) {
194 if (documentOriginalTitle === '') {
196 if (documentOriginalTitle === '') {
195 documentOriginalTitle = document.title;
197 documentOriginalTitle = document.title;
196 }
198 }
197 unreadPosts = unreadPosts + newPostCount;
199 unreadPosts = unreadPosts + newPostCount;
198 document.title = '[' + unreadPosts + '] ' + documentOriginalTitle;
200 document.title = '[' + unreadPosts + '] ' + documentOriginalTitle;
199
201
200 document.addEventListener('visibilitychange', function() {
202 document.addEventListener('visibilitychange', function() {
201 if (documentOriginalTitle !== '') {
203 if (documentOriginalTitle !== '') {
202 document.title = documentOriginalTitle;
204 document.title = documentOriginalTitle;
203 documentOriginalTitle = '';
205 documentOriginalTitle = '';
204 unreadPosts = 0;
206 unreadPosts = 0;
205 }
207 }
206
208
207 document.removeEventListener('visibilitychange', null);
209 document.removeEventListener('visibilitychange', null);
208 });
210 });
209 }
211 }
210 }
212 }
211
213
212 /**
214 /**
213 * Clear all entered values in the form fields
215 * Clear all entered values in the form fields
214 */
216 */
215 function resetForm(form) {
217 function resetForm(form) {
216 form.find('input:text, input:password, input:file, select, textarea').val('');
218 form.find('input:text, input:password, input:file, select, textarea').val('');
217 form.find('input:radio, input:checkbox')
219 form.find('input:radio, input:checkbox')
218 .removeAttr('checked').removeAttr('selected');
220 .removeAttr('checked').removeAttr('selected');
219 $('.file_wrap').find('.file-thumb').remove();
221 $('.file_wrap').find('.file-thumb').remove();
220 }
222 }
221
223
222 /**
224 /**
223 * When the form is posted, this method will be run as a callback
225 * When the form is posted, this method will be run as a callback
224 */
226 */
225 function updateOnPost(response, statusText, xhr, form) {
227 function updateOnPost(response, statusText, xhr, form) {
226 var json = $.parseJSON(response);
228 var json = $.parseJSON(response);
227 var status = json.status;
229 var status = json.status;
228
230
229 form.children('.form-errors').remove();
231 form.children('.form-errors').remove();
230
232
231 if (status === 'ok') {
233 if (status === 'ok') {
232 resetForm(form);
234 resetForm(form);
233 updateThread();
235 updateThread();
234 } else {
236 } else {
235 var errors = json.errors;
237 var errors = json.errors;
236 for (var i = 0; i < errors.length; i++) {
238 for (var i = 0; i < errors.length; i++) {
237 var fieldErrors = errors[i];
239 var fieldErrors = errors[i];
238
240
239 var error = fieldErrors.errors;
241 var error = fieldErrors.errors;
240
242
241 var errorList = $('<div class="form-errors">' + error
243 var errorList = $('<div class="form-errors">' + error
242 + '<div>');
244 + '<div>');
243 errorList.appendTo(form);
245 errorList.appendTo(form);
244 }
246 }
245 }
247 }
246 }
248 }
247
249
248 $(document).ready(function(){
250 $(document).ready(function(){
249 initAutoupdate();
251 initAutoupdate();
250
252
251 // Post form data over AJAX
253 // Post form data over AJAX
252 var threadId = $('div.thread').children('.post').first().attr('id');;
254 var threadId = $('div.thread').children('.post').first().attr('id');;
253
255
254 var form = $('#form');
256 var form = $('#form');
255
257
256 var options = {
258 var options = {
257 success: updateOnPost,
259 success: updateOnPost,
258 url: '/api/add_post/' + threadId + '/'
260 url: '/api/add_post/' + threadId + '/'
259 };
261 };
260
262
261 form.ajaxForm(options);
263 form.ajaxForm(options);
262
264
263 resetForm(form);
265 resetForm(form);
264 });
266 });
@@ -1,62 +1,62 b''
1 {% load staticfiles %}
1 {% load staticfiles %}
2 {% load i18n %}
2 {% load i18n %}
3 {% load l10n %}
3 {% load l10n %}
4 {% load static from staticfiles %}
4 {% load static from staticfiles %}
5
5
6 <!DOCTYPE html>
6 <!DOCTYPE html>
7 <html>
7 <html>
8 <head>
8 <head>
9 <link rel="stylesheet" type="text/css"
9 <link rel="stylesheet" type="text/css" href="{% static 'css/base.css' %}" media="all"/>
10 href="{% static 'css/base.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"
11 <link rel="stylesheet" type="text/css" href="{% static theme_css %}" media="all"/>
12 href="{% static theme_css %}" media="all"/>
12 <link rel="alternate" type="application/rss+xml" href="rss/" title="{% trans 'Feed' %}"/>
13 <link rel="alternate" type="application/rss+xml" href="rss/" title=
14 "{% trans 'Feed' %}"/>
15
13
16 <link rel="icon" type="image/png"
14 <link rel="icon" type="image/png"
17 href="{% static 'favicon.png' %}">
15 href="{% static 'favicon.png' %}">
18
16
19 <meta name="viewport" content="width=device-width, initial-scale=1"/>
17 <meta name="viewport" content="width=device-width, initial-scale=1"/>
20 <meta charset="utf-8"/>
18 <meta charset="utf-8"/>
21
19
22 {% block head %}{% endblock %}
20 {% block head %}{% endblock %}
23 </head>
21 </head>
24 <body>
22 <body>
25 <script src="{% static 'js/jquery-2.0.1.min.js' %}"></script>
23 <script src="{% static 'js/jquery-2.0.1.min.js' %}"></script>
26 <script src="{% static 'js/jquery-ui-1.10.3.custom.min.js' %}"></script>
24 <script src="{% static 'js/jquery-ui-1.10.3.custom.min.js' %}"></script>
27 <script src="{% static 'js/jquery.mousewheel.js' %}"></script>
25 <script src="{% static 'js/jquery.mousewheel.js' %}"></script>
28 <script src="{% url 'js_info_dict' %}"></script>
26 <script src="{% url 'js_info_dict' %}"></script>
29
27
30 <div class="navigation_panel">
28 <div class="navigation_panel">
31 <a class="link" href="{% url 'index' %}">{% trans "All threads" %}</a>
29 <a class="link" href="{% url 'index' %}">{% trans "All threads" %}</a>
32 {% for tag in tags %}
30 {% for tag in tags %}
33 <a class="tag" href="{% url 'tag' tag_name=tag.name %}"
31 <a class="tag" href="{% url 'tag' tag_name=tag.name %}"
34 >#{{ tag.name }}</a>,
32 >#{{ tag.name }}</a>,
35 {% endfor %}
33 {% endfor %}
36 <a href="{% url 'tags' %}" title="{% trans 'Tag management' %}"
34 <a href="{% url 'tags' %}" title="{% trans 'Tag management' %}"
37 >[...]</a>
35 >[...]</a>
38 <a class="link" href="{% url 'settings' %}">{% trans 'Settings' %}</a>
36 <a class="link" href="{% url 'settings' %}">{% trans 'Settings' %}</a>
39 </div>
37 </div>
40
38
41 {% block content %}{% endblock %}
39 {% block content %}{% endblock %}
42
40
43 <script src="{% static 'js/popup.js' %}"></script>
41 <script src="{% static 'js/popup.js' %}"></script>
44 <script src="{% static 'js/image.js' %}"></script>
42 <script src="{% static 'js/image.js' %}"></script>
45 <script src="{% static 'js/refpopup.js' %}"></script>
43 <script src="{% static 'js/refpopup.js' %}"></script>
44 <script src="{% static 'js/3party/highlight.min.js' %}"></script>
46 <script src="{% static 'js/main.js' %}"></script>
45 <script src="{% static 'js/main.js' %}"></script>
47
46
48 <div class="navigation_panel">
47 <div class="navigation_panel">
49 {% block metapanel %}{% endblock %}
48 {% block metapanel %}{% endblock %}
50 [<a href="{% url "login" %}">{% trans 'Login' %}</a>]
49 [<a href="{% url "login" %}">{% trans 'Login' %}</a>]
50 [<a href="{% url "haystack_search" %}">{% trans 'Search' %}</a>]
51 {% with ppd=posts_per_day|floatformat:2 %}
51 {% with ppd=posts_per_day|floatformat:2 %}
52 {% blocktrans %}Speed: {{ ppd }} posts per day{% endblocktrans %}
52 {% blocktrans %}Speed: {{ ppd }} posts per day{% endblocktrans %}
53 {% endwith %}
53 {% endwith %}
54 <a class="link" href="#top">{% trans 'Up' %}</a>
54 <a class="link" href="#top">{% trans 'Up' %}</a>
55 </div>
55 </div>
56
56
57 <div class="footer">
57 <div class="footer">
58 <!-- Put your banners here -->
58 <!-- Put your banners here -->
59 </div>
59 </div>
60
60
61 </body>
61 </body>
62 </html>
62 </html>
@@ -1,96 +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.image %}
49 {% if post.images.exists %}
50 {% with post.images.all.0 as image %}
50 <div class="image">
51 <div class="image">
51 <a
52 <a
52 class="thumb"
53 class="thumb"
53 href="{{ post.image.url }}"><img
54 href="{{ image.image.url }}"><img
54 src="{{ post.image.url_200x150 }}"
55 src="{{ image.image.url_200x150 }}"
55 alt="{{ post.id }}"
56 alt="{{ post.id }}"
56 width="{{ post.image_pre_width }}"
57 width="{{ image.pre_width }}"
57 height="{{ post.image_pre_height }}"
58 height="{{ image.pre_height }}"
58 data-width="{{ post.image_width }}"
59 data-width="{{ image.width }}"
59 data-height="{{ post.image_height }}"/>
60 data-height="{{ image.height }}"/>
60 </a>
61 </a>
61 </div>
62 </div>
63 {% endwith %}
62 {% endif %}
64 {% endif %}
63 <div class="message">
65 <div class="message">
64 {% autoescape off %}
66 {% autoescape off %}
65 {% if truncated %}
67 {% if truncated %}
66 {{ post.text.rendered|truncatewords_html:50 }}
68 {{ post.text.rendered|truncatewords_html:50 }}
67 {% else %}
69 {% else %}
68 {{ post.text.rendered }}
70 {{ post.text.rendered }}
69 {% endif %}
71 {% endif %}
70 {% endautoescape %}
72 {% endautoescape %}
71 {% if post.is_referenced %}
73 {% if post.is_referenced %}
72 <div class="refmap">
74 <div class="refmap">
73 {% autoescape off %}
75 {% autoescape off %}
74 {% trans "Replies" %}: {{ post.refmap }}
76 {% trans "Replies" %}: {{ post.refmap }}
75 {% endautoescape %}
77 {% endautoescape %}
76 </div>
78 </div>
77 {% endif %}
79 {% endif %}
78 </div>
80 </div>
79 {% endcache %}
81 {% endcache %}
80 {% if is_opening %}
82 {% if is_opening %}
81 {% 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 %}
82 <div class="metadata">
84 <div class="metadata">
83 {% if is_opening and need_open_link %}
85 {% if is_opening and need_open_link %}
86 {{ thread.get_reply_count }} {% trans 'replies' %},
84 {{ thread.get_images_count }} {% trans 'images' %}.
87 {{ thread.get_images_count }} {% trans 'images' %}.
85 {% endif %}
88 {% endif %}
86 <span class="tags">
89 <span class="tags">
87 {% for tag in thread.get_tags %}
90 {% for tag in thread.get_tags %}
88 <a class="tag" href="{% url 'tag' tag.name %}">
91 <a class="tag" href="{% url 'tag' tag.name %}">
89 #{{ tag.name }}</a>{% if not forloop.last %},{% endif %}
92 #{{ tag.name }}</a>{% if not forloop.last %},{% endif %}
90 {% endfor %}
93 {% endfor %}
91 </span>
94 </span>
92 </div>
95 </div>
93 {% endcache %}
96 {% endcache %}
94 {% endif %}
97 {% endif %}
95 </div>
98 </div>
96 {% endspaceless %}
99 {% endspaceless %}
@@ -1,15 +1,15 b''
1 {% load i18n %}
1 {% load i18n %}
2
2
3 {% if obj.image %}
3 {% if obj.images.exists %}
4 <img src="{{ obj.image.url_200x150 }}"
4 <img src="{{ obj.get_first_image.image.url_200x150 }}"
5 alt="{% trans 'Post image' %}" />
5 alt="{% trans 'Post image' %}" />
6 {% endif %}
6 {% endif %}
7 {{ obj.text.rendered|safe }}
7 {{ obj.text.rendered|safe }}
8 {% if obj.tags.all %}
8 {% if obj.tags.all %}
9 <p>
9 <p>
10 {% trans 'Tags' %}:
10 {% trans 'Tags' %}:
11 {% for tag in obj.tags.all %}
11 {% for tag in obj.tags.all %}
12 {{ tag.name }}
12 {{ tag.name }}
13 {% endfor %}
13 {% endfor %}
14 </p>
14 </p>
15 {% endif %} No newline at end of file
15 {% endif %}
@@ -1,96 +1,3 b''
1 {% load i18n %}
1 <div class="post">
2 {% load board %}
2 <a class="tag" href="{% url 'tag' tag_name=tag.name %}">#{{ tag.name }}</a>
3 {% load cache %}
3 </div> No newline at end of file
4
5 {% get_current_language as LANGUAGE_CODE %}
6
7 {% spaceless %}
8 {% cache 600 post post.id post.last_edit_time thread.archived bumpable truncated moderator LANGUAGE_CODE need_open_link %}
9 {% if thread.archived %}
10 <div class="post archive_post" id="{{ post.id }}">
11 {% elif bumpable %}
12 <div class="post" id="{{ post.id }}">
13 {% else %}
14 <div class="post dead_post" id="{{ post.id }}">
15 {% endif %}
16
17 <div class="post-info">
18 <a class="post_id" href="{% post_object_url post thread=thread %}"
19 {% if not truncated and not thread.archived %}
20 onclick="javascript:addQuickReply('{{ post.id }}'); return false;"
21 title="{% trans 'Quote' %}"
22 {% endif %}
23 >({{ post.id }}) </a>
24 <span class="title">{{ post.title }} </span>
25 <span class="pub_time">{{ post.pub_time }}</span>
26 {% if thread.archived %}
27 β€” {{ thread.bump_time }}
28 {% endif %}
29 {% if is_opening and need_open_link %}
30 {% if thread.archived %}
31 [<a class="link" href="{% url 'thread' post.id %}">{% trans "Open" %}</a>]
32 {% else %}
33 [<a class="link" href="{% url 'thread' post.id %}#form">{% trans "Reply" %}</a>]
34 {% endif %}
35 {% endif %}
36
37 {% if moderator %}
38 <span class="moderator_info">
39 [<a href="{% url 'post_admin' post_id=post.id %}"
40 >{% trans 'Edit' %}</a>]
41 [<a href="{% url 'delete' post_id=post.id %}"
42 >{% trans 'Delete' %}</a>]
43 ({{ post.poster_ip }})
44 [<a href="{% url 'ban' post_id=post.id %}?next={{ request.path }}"
45 >{% trans 'Ban IP' %}</a>]
46 </span>
47 {% endif %}
48 </div>
49 {% if post.image %}
50 <div class="image">
51 <a
52 class="thumb"
53 href="{{ post.image.url }}"><img
54 src="{{ post.image.url_200x150 }}"
55 alt="{{ post.id }}"
56 width="{{ post.image_pre_width }}"
57 height="{{ post.image_pre_height }}"
58 data-width="{{ post.image_width }}"
59 data-height="{{ post.image_height }}"/>
60 </a>
61 </div>
62 {% endif %}
63 <div class="message">
64 {% autoescape off %}
65 {% if truncated %}
66 {{ post.text.rendered|truncatewords_html:50 }}
67 {% else %}
68 {{ post.text.rendered }}
69 {% endif %}
70 {% endautoescape %}
71 {% if post.is_referenced %}
72 <div class="refmap">
73 {% autoescape off %}
74 {% trans "Replies" %}: {{ post.refmap }}
75 {% endautoescape %}
76 </div>
77 {% endif %}
78 </div>
79 {% endcache %}
80 {% if is_opening %}
81 {% cache 600 post_thread thread.id thread.last_edit_time LANGUAGE_CODE need_open_link %}
82 <div class="metadata">
83 {% if is_opening and need_open_link %}
84 {{ thread.get_images_count }} {% trans 'images' %}.
85 {% endif %}
86 <span class="tags">
87 {% for tag in thread.get_tags %}
88 <a class="tag" href="{% url 'tag' tag.name %}">
89 #{{ tag.name }}</a>{% if not forloop.last %},{% endif %}
90 {% endfor %}
91 </span>
92 </div>
93 {% endcache %}
94 {% endif %}
95 </div>
96 {% endspaceless %}
@@ -1,108 +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-compact">
53 <form enctype="multipart/form-data" method="post"
54 >{% csrf_token %}
55 <input type="file" name="image" accept="image/*"/>
56 <div class="compact-form-text">
57 <textarea name="text" style="height: 100px;
58 width: 100%;
59 "></textarea>
60 </div>
61 <div class="form-submit">
62 <input type="submit" value="{% trans "Post" %}"/>
63 </div>
64 </form>
65 </div>
66 <div class="swappable-form-full">
52 <div class="swappable-form-full">
67 <form enctype="multipart/form-data" method="post"
53 <form enctype="multipart/form-data" method="post"
68 >{% csrf_token %}
54 >{% csrf_token %}
55 <div class="compact-form-text"></div>
69 {{ form.as_div }}
56 {{ form.as_div }}
70 <div class="form-submit">
57 <div class="form-submit">
71 <input type="submit" value="{% trans "Post" %}"/>
58 <input type="submit" value="{% trans "Post" %}"/>
72 </div>
59 </div>
73 </form>
60 </form>
74 </div>
61 </div>
75 <a onclick="swapForm(); return false;" href="#">
62 <a onclick="swapForm(); return false;" href="#">
76 {% trans 'Switch mode' %}
63 {% trans 'Switch mode' %}
77 </a>
64 </a>
78 <div><a href="{% url "staticpage" name="help" %}">
65 <div><a href="{% url "staticpage" name="help" %}">
79 {% trans 'Text syntax' %}</a></div>
66 {% trans 'Text syntax' %}</a></div>
80 </div>
67 </div>
81 </div>
68 </div>
82
69
83 <script src="{% static 'js/jquery.form.min.js' %}"></script>
70 <script src="{% static 'js/jquery.form.min.js' %}"></script>
84 <script src="{% static 'js/thread_update.js' %}"></script>
71 <script src="{% static 'js/thread_update.js' %}"></script>
85 {% endif %}
72 {% endif %}
86
73
87 <script src="{% static 'js/form.js' %}"></script>
74 <script src="{% static 'js/form.js' %}"></script>
88 <script src="{% static 'js/thread.js' %}"></script>
75 <script src="{% static 'js/thread.js' %}"></script>
89
76
90 {% endcache %}
77 {% endcache %}
91
78
92 {% endspaceless %}
79 {% endspaceless %}
93 {% endblock %}
80 {% endblock %}
94
81
95 {% block metapanel %}
82 {% block metapanel %}
96
83
97 {% get_current_language as LANGUAGE_CODE %}
84 {% get_current_language as LANGUAGE_CODE %}
98
85
99 <span class="metapanel" data-last-update="{{ last_update }}">
86 <span class="metapanel" data-last-update="{{ last_update }}">
100 {% cache 600 thread_meta thread.last_edit_time moderator LANGUAGE_CODE %}
87 {% cache 600 thread_meta thread.last_edit_time moderator LANGUAGE_CODE %}
101 <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 'replies' %},
102 <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}.
89 <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}.
103 {% 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>
104 [<a href="rss/">RSS</a>]
91 [<a href="rss/">RSS</a>]
105 {% endcache %}
92 {% endcache %}
106 </span>
93 </span>
107
94
108 {% endblock %}
95 {% endblock %}
@@ -1,66 +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 <div>
29 <div>
29 <a
30 <a
30 class="thumb"
31 class="thumb"
31 href="{{ post.image.url }}"><img
32 href="{{ image.image.url }}"><img
32 src="{{ post.image.url_200x150 }}"
33 src="{{ image.image.url_200x150 }}"
33 alt="{{ post.id }}"
34 alt="{{ post.id }}"
34 width="{{ post.image_pre_width }}"
35 width="{{ image.pre_width }}"
35 height="{{ post.image_pre_height }}"
36 height="{{ image.pre_height }}"
36 data-width="{{ post.image_width }}"
37 data-width="{{ image.width }}"
37 data-height="{{ post.image_height }}"/>
38 data-height="{{ image.height }}"/>
38 </a>
39 </a>
39 </div>
40 </div>
40 <div class="gallery_image_metadata">
41 <div class="gallery_image_metadata">
41 {{ post.image_width }}x{{ post.image_height }}
42 {{ image.width }}x{{ image.height }}
42 {% image_actions post.image.url request.get_host %}
43 {% image_actions image.image.url request.get_host %}
43 </div>
44 </div>
45 {% endwith %}
44 </div>
46 </div>
45 {% endfor %}
47 {% endfor %}
46 </div>
48 </div>
47 {% endcache %}
49 {% endcache %}
48
50
49 {% endspaceless %}
51 {% endspaceless %}
50 {% endblock %}
52 {% endblock %}
51
53
52 {% block metapanel %}
54 {% block metapanel %}
53
55
54 {% get_current_language as LANGUAGE_CODE %}
56 {% get_current_language as LANGUAGE_CODE %}
55
57
56 <span class="metapanel" data-last-update="{{ last_update }}">
58 <span class="metapanel" data-last-update="{{ last_update }}">
57 {% cache 600 thread_meta thread.last_edit_time moderator LANGUAGE_CODE %}
59 {% cache 600 thread_meta thread.last_edit_time moderator LANGUAGE_CODE %}
58 <span id="reply-count">{{ thread.get_reply_count }}</span>/{{ max_replies }}
60 <span id="reply-count">{{ thread.get_reply_count }}</span>/{{ max_replies }}
59 {% trans 'replies' %},
61 {% trans 'replies' %},
60 <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}.
62 <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}.
61 {% trans 'Last update: ' %}{{ thread.last_edit_time }}
63 {% trans 'Last update: ' %}{{ thread.last_edit_time }}
62 [<a href="rss/">RSS</a>]
64 [<a href="rss/">RSS</a>]
63 {% endcache %}
65 {% endcache %}
64 </span>
66 </span>
65
67
66 {% endblock %}
68 {% endblock %}
@@ -1,89 +1,89 b''
1 from django.shortcuts import get_object_or_404
1 from django.shortcuts import get_object_or_404
2 from boards.models import Post
3 from boards.views import thread, api
4 from django import template
2 from django import template
5
3
4
6 register = template.Library()
5 register = template.Library()
7
6
8 actions = [
7 actions = [
9 {
8 {
10 'name': 'google',
9 'name': 'google',
11 'link': 'http://google.com/searchbyimage?image_url=%s',
10 'link': 'http://google.com/searchbyimage?image_url=%s',
12 },
11 },
13 {
12 {
14 'name': 'iqdb',
13 'name': 'iqdb',
15 'link': 'http://iqdb.org/?url=%s',
14 'link': 'http://iqdb.org/?url=%s',
16 },
15 },
17 ]
16 ]
18
17
19
18
20 @register.simple_tag(name='post_url')
19 @register.simple_tag(name='post_url')
21 def post_url(*args, **kwargs):
20 def post_url(*args, **kwargs):
22 post_id = args[0]
21 post_id = args[0]
23
22
24 post = get_object_or_404(Post, id=post_id)
23 post = get_object_or_404('Post', id=post_id)
25
24
26 return post.get_url()
25 return post.get_url()
27
26
28
27
29 @register.simple_tag(name='post_object_url')
28 @register.simple_tag(name='post_object_url')
30 def post_object_url(*args, **kwargs):
29 def post_object_url(*args, **kwargs):
31 post = args[0]
30 post = args[0]
32
31
33 if 'thread' in kwargs:
32 if 'thread' in kwargs:
34 post_thread = kwargs['thread']
33 post_thread = kwargs['thread']
35 else:
34 else:
36 post_thread = None
35 post_thread = None
37
36
38 return post.get_url(thread=post_thread)
37 return post.get_url(thread=post_thread)
39
38
40
39
41 @register.simple_tag(name='image_actions')
40 @register.simple_tag(name='image_actions')
42 def image_actions(*args, **kwargs):
41 def image_actions(*args, **kwargs):
43 image_link = args[0]
42 image_link = args[0]
44 if len(args) > 1:
43 if len(args) > 1:
45 image_link = 'http://' + args[1] + image_link # TODO https?
44 image_link = 'http://' + args[1] + image_link # TODO https?
46
45
47 result = ''
46 result = ''
48
47
49 for action in actions:
48 for action in actions:
50 result += '[<a href="' + action['link'] % image_link + '">' + \
49 result += '[<a href="' + action['link'] % image_link + '">' + \
51 action['name'] + '</a>]'
50 action['name'] + '</a>]'
52
51
53 return result
52 return result
54
53
55
54
55 # TODO Use get_view of a post instead of this
56 @register.inclusion_tag('boards/post.html', name='post_view')
56 @register.inclusion_tag('boards/post.html', name='post_view')
57 def post_view(post, moderator=False, need_open_link=False, truncated=False,
57 def post_view(post, moderator=False, need_open_link=False, truncated=False,
58 **kwargs):
58 **kwargs):
59 """
59 """
60 Get post
60 Get post
61 """
61 """
62
62
63 if 'is_opening' in kwargs:
63 if 'is_opening' in kwargs:
64 is_opening = kwargs['is_opening']
64 is_opening = kwargs['is_opening']
65 else:
65 else:
66 is_opening = post.is_opening()
66 is_opening = post.is_opening()
67
67
68 if 'thread' in kwargs:
68 if 'thread' in kwargs:
69 thread = kwargs['thread']
69 thread = kwargs['thread']
70 else:
70 else:
71 thread = post.get_thread()
71 thread = post.get_thread()
72
72
73 if 'can_bump' in kwargs:
73 if 'can_bump' in kwargs:
74 can_bump = kwargs['can_bump']
74 can_bump = kwargs['can_bump']
75 else:
75 else:
76 can_bump = thread.can_bump()
76 can_bump = thread.can_bump()
77
77
78 opening_post_id = thread.get_opening_post_id()
78 opening_post_id = thread.get_opening_post_id()
79
79
80 return {
80 return {
81 'post': post,
81 'post': post,
82 'moderator': moderator,
82 'moderator': moderator,
83 'is_opening': is_opening,
83 'is_opening': is_opening,
84 'thread': thread,
84 'thread': thread,
85 'bumpable': can_bump,
85 'bumpable': can_bump,
86 'need_open_link': need_open_link,
86 'need_open_link': need_open_link,
87 'truncated': truncated,
87 'truncated': truncated,
88 'opening_post_id': opening_post_id,
88 'opening_post_id': opening_post_id,
89 }
89 }
@@ -1,260 +1,259 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
9
10 from boards.models import Post, Tag, Thread
10 from boards.models import Post, Tag, Thread
11 from boards import urls
11 from boards import urls
12 from neboard import settings
12 from neboard import settings
13
13
14 PAGE_404 = 'boards/404.html'
14 PAGE_404 = 'boards/404.html'
15
15
16 TEST_TEXT = 'test text'
16 TEST_TEXT = 'test text'
17
17
18 NEW_THREAD_PAGE = '/'
18 NEW_THREAD_PAGE = '/'
19 THREAD_PAGE_ONE = '/thread/1/'
19 THREAD_PAGE_ONE = '/thread/1/'
20 THREAD_PAGE = '/thread/'
20 THREAD_PAGE = '/thread/'
21 TAG_PAGE = '/tag/'
21 TAG_PAGE = '/tag/'
22 HTTP_CODE_REDIRECT = 302
22 HTTP_CODE_REDIRECT = 302
23 HTTP_CODE_OK = 200
23 HTTP_CODE_OK = 200
24 HTTP_CODE_NOT_FOUND = 404
24 HTTP_CODE_NOT_FOUND = 404
25
25
26 logger = logging.getLogger(__name__)
26 logger = logging.getLogger(__name__)
27
27
28
28
29 class PostTests(TestCase):
29 class PostTests(TestCase):
30
30
31 def _create_post(self):
31 def _create_post(self):
32 return Post.objects.create_post(title='title',
32 return Post.objects.create_post(title='title',
33 text='text')
33 text='text')
34
34
35 def test_post_add(self):
35 def test_post_add(self):
36 """Test adding post"""
36 """Test adding post"""
37
37
38 post = self._create_post()
38 post = self._create_post()
39
39
40 self.assertIsNotNone(post, 'No post was created')
40 self.assertIsNotNone(post, 'No post was created')
41
41
42 def test_delete_post(self):
42 def test_delete_post(self):
43 """Test post deletion"""
43 """Test post deletion"""
44
44
45 post = self._create_post()
45 post = self._create_post()
46 post_id = post.id
46 post_id = post.id
47
47
48 Post.objects.delete_post(post)
48 Post.objects.delete_post(post)
49
49
50 self.assertFalse(Post.objects.filter(id=post_id).exists())
50 self.assertFalse(Post.objects.filter(id=post_id).exists())
51
51
52 def test_delete_thread(self):
52 def test_delete_thread(self):
53 """Test thread deletion"""
53 """Test thread deletion"""
54
54
55 opening_post = self._create_post()
55 opening_post = self._create_post()
56 thread = opening_post.get_thread()
56 thread = opening_post.get_thread()
57 reply = Post.objects.create_post("", "", thread=thread)
57 reply = Post.objects.create_post("", "", thread=thread)
58
58
59 thread.delete_with_posts()
59 thread.delete_with_posts()
60
60
61 self.assertFalse(Post.objects.filter(id=reply.id).exists())
61 self.assertFalse(Post.objects.filter(id=reply.id).exists())
62
62
63 def test_post_to_thread(self):
63 def test_post_to_thread(self):
64 """Test adding post to a thread"""
64 """Test adding post to a thread"""
65
65
66 op = self._create_post()
66 op = self._create_post()
67 post = Post.objects.create_post("", "", thread=op.get_thread())
67 post = Post.objects.create_post("", "", thread=op.get_thread())
68
68
69 self.assertIsNotNone(post, 'Reply to thread wasn\'t created')
69 self.assertIsNotNone(post, 'Reply to thread wasn\'t created')
70 self.assertEqual(op.get_thread().last_edit_time, post.pub_time,
70 self.assertEqual(op.get_thread().last_edit_time, post.pub_time,
71 'Post\'s create time doesn\'t match thread last edit'
71 'Post\'s create time doesn\'t match thread last edit'
72 ' time')
72 ' time')
73
73
74 def test_delete_posts_by_ip(self):
74 def test_delete_posts_by_ip(self):
75 """Test deleting posts with the given ip"""
75 """Test deleting posts with the given ip"""
76
76
77 post = self._create_post()
77 post = self._create_post()
78 post_id = post.id
78 post_id = post.id
79
79
80 Post.objects.delete_posts_by_ip('0.0.0.0')
80 Post.objects.delete_posts_by_ip('0.0.0.0')
81
81
82 self.assertFalse(Post.objects.filter(id=post_id).exists())
82 self.assertFalse(Post.objects.filter(id=post_id).exists())
83
83
84 def test_get_thread(self):
84 def test_get_thread(self):
85 """Test getting all posts of a thread"""
85 """Test getting all posts of a thread"""
86
86
87 opening_post = self._create_post()
87 opening_post = self._create_post()
88
88
89 for i in range(0, 2):
89 for i in range(0, 2):
90 Post.objects.create_post('title', 'text',
90 Post.objects.create_post('title', 'text',
91 thread=opening_post.get_thread())
91 thread=opening_post.get_thread())
92
92
93 thread = opening_post.get_thread()
93 thread = opening_post.get_thread()
94
94
95 self.assertEqual(3, thread.replies.count())
95 self.assertEqual(3, thread.replies.count())
96
96
97 def test_create_post_with_tag(self):
97 def test_create_post_with_tag(self):
98 """Test adding tag to post"""
98 """Test adding tag to post"""
99
99
100 tag = Tag.objects.create(name='test_tag')
100 tag = Tag.objects.create(name='test_tag')
101 post = Post.objects.create_post(title='title', text='text', tags=[tag])
101 post = Post.objects.create_post(title='title', text='text', tags=[tag])
102
102
103 thread = post.get_thread()
103 thread = post.get_thread()
104 self.assertIsNotNone(post, 'Post not created')
104 self.assertIsNotNone(post, 'Post not created')
105 self.assertTrue(tag in thread.tags.all(), 'Tag not added to thread')
105 self.assertTrue(tag in thread.tags.all(), 'Tag not added to thread')
106 self.assertTrue(thread in tag.threads.all(), 'Thread not added to tag')
106 self.assertTrue(thread in tag.threads.all(), 'Thread not added to tag')
107
107
108 def test_thread_max_count(self):
108 def test_thread_max_count(self):
109 """Test deletion of old posts when the max thread count is reached"""
109 """Test deletion of old posts when the max thread count is reached"""
110
110
111 for i in range(settings.MAX_THREAD_COUNT + 1):
111 for i in range(settings.MAX_THREAD_COUNT + 1):
112 self._create_post()
112 self._create_post()
113
113
114 self.assertEqual(settings.MAX_THREAD_COUNT,
114 self.assertEqual(settings.MAX_THREAD_COUNT,
115 len(Thread.objects.filter(archived=False)))
115 len(Thread.objects.filter(archived=False)))
116
116
117 def test_pages(self):
117 def test_pages(self):
118 """Test that the thread list is properly split into pages"""
118 """Test that the thread list is properly split into pages"""
119
119
120 for i in range(settings.MAX_THREAD_COUNT):
120 for i in range(settings.MAX_THREAD_COUNT):
121 self._create_post()
121 self._create_post()
122
122
123 all_threads = Thread.objects.filter(archived=False)
123 all_threads = Thread.objects.filter(archived=False)
124
124
125 paginator = Paginator(Thread.objects.filter(archived=False),
125 paginator = Paginator(Thread.objects.filter(archived=False),
126 settings.THREADS_PER_PAGE)
126 settings.THREADS_PER_PAGE)
127 posts_in_second_page = paginator.page(2).object_list
127 posts_in_second_page = paginator.page(2).object_list
128 first_post = posts_in_second_page[0]
128 first_post = posts_in_second_page[0]
129
129
130 self.assertEqual(all_threads[settings.THREADS_PER_PAGE].id,
130 self.assertEqual(all_threads[settings.THREADS_PER_PAGE].id,
131 first_post.id)
131 first_post.id)
132
132
133 def test_linked_tag(self):
133 def test_linked_tag(self):
134 """Test adding a linked tag"""
134 """Test adding a linked tag"""
135
135
136 linked_tag = Tag.objects.create(name=u'tag1')
136 linked_tag = Tag.objects.create(name=u'tag1')
137 tag = Tag.objects.create(name=u'tag2', linked=linked_tag)
137 tag = Tag.objects.create(name=u'tag2', linked=linked_tag)
138
138
139 post = Post.objects.create_post("", "", tags=[tag])
139 post = Post.objects.create_post("", "", tags=[tag])
140
140
141 self.assertTrue(linked_tag in post.get_thread().tags.all(),
141 self.assertTrue(linked_tag in post.get_thread().tags.all(),
142 'Linked tag was not added')
142 'Linked tag was not added')
143
143
144
144
145 class PagesTest(TestCase):
145 class PagesTest(TestCase):
146
146
147 def test_404(self):
147 def test_404(self):
148 """Test receiving error 404 when opening a non-existent page"""
148 """Test receiving error 404 when opening a non-existent page"""
149
149
150 tag_name = u'test_tag'
150 tag_name = u'test_tag'
151 tag = Tag.objects.create(name=tag_name)
151 tag = Tag.objects.create(name=tag_name)
152 client = Client()
152 client = Client()
153
153
154 Post.objects.create_post('title', TEST_TEXT, tags=[tag])
154 Post.objects.create_post('title', TEST_TEXT, tags=[tag])
155
155
156 existing_post_id = Post.objects.all()[0].id
156 existing_post_id = Post.objects.all()[0].id
157 response_existing = client.get(THREAD_PAGE + str(existing_post_id) +
157 response_existing = client.get(THREAD_PAGE + str(existing_post_id) +
158 '/')
158 '/')
159 self.assertEqual(HTTP_CODE_OK, response_existing.status_code,
159 self.assertEqual(HTTP_CODE_OK, response_existing.status_code,
160 u'Cannot open existing thread')
160 u'Cannot open existing thread')
161
161
162 response_not_existing = client.get(THREAD_PAGE + str(
162 response_not_existing = client.get(THREAD_PAGE + str(
163 existing_post_id + 1) + '/')
163 existing_post_id + 1) + '/')
164 self.assertEqual(PAGE_404,
164 self.assertEqual(PAGE_404, response_not_existing.templates[0].name,
165 response_not_existing.templates[0].name,
166 u'Not existing thread is opened')
165 u'Not existing thread is opened')
167
166
168 response_existing = client.get(TAG_PAGE + tag_name + '/')
167 response_existing = client.get(TAG_PAGE + tag_name + '/')
169 self.assertEqual(HTTP_CODE_OK,
168 self.assertEqual(HTTP_CODE_OK,
170 response_existing.status_code,
169 response_existing.status_code,
171 u'Cannot open existing tag')
170 u'Cannot open existing tag')
172
171
173 response_not_existing = client.get(TAG_PAGE + u'not_tag' + '/')
172 response_not_existing = client.get(TAG_PAGE + u'not_tag' + '/')
174 self.assertEqual(PAGE_404,
173 self.assertEqual(PAGE_404,
175 response_not_existing.templates[0].name,
174 response_not_existing.templates[0].name,
176 u'Not existing tag is opened')
175 u'Not existing tag is opened')
177
176
178 reply_id = Post.objects.create_post('', TEST_TEXT,
177 reply_id = Post.objects.create_post('', TEST_TEXT,
179 thread=Post.objects.all()[0]
178 thread=Post.objects.all()[0]
180 .thread)
179 .get_thread())
181 response_not_existing = client.get(THREAD_PAGE + str(
180 response_not_existing = client.get(THREAD_PAGE + str(
182 reply_id) + '/')
181 reply_id) + '/')
183 self.assertEqual(PAGE_404,
182 self.assertEqual(PAGE_404,
184 response_not_existing.templates[0].name,
183 response_not_existing.templates[0].name,
185 u'Reply is opened as a thread')
184 u'Reply is opened as a thread')
186
185
187
186
188 class FormTest(TestCase):
187 class FormTest(TestCase):
189 def test_post_validation(self):
188 def test_post_validation(self):
190 # Disable captcha for the test
189 # Disable captcha for the test
191 captcha_enabled = settings.ENABLE_CAPTCHA
190 captcha_enabled = settings.ENABLE_CAPTCHA
192 settings.ENABLE_CAPTCHA = False
191 settings.ENABLE_CAPTCHA = False
193
192
194 client = Client()
193 client = Client()
195
194
196 valid_tags = u'tag1 tag_2 Ρ‚Π΅Π³_3'
195 valid_tags = u'tag1 tag_2 Ρ‚Π΅Π³_3'
197 invalid_tags = u'$%_356 ---'
196 invalid_tags = u'$%_356 ---'
198
197
199 response = client.post(NEW_THREAD_PAGE, {'title': 'test title',
198 response = client.post(NEW_THREAD_PAGE, {'title': 'test title',
200 'text': TEST_TEXT,
199 'text': TEST_TEXT,
201 'tags': valid_tags})
200 'tags': valid_tags})
202 self.assertEqual(response.status_code, HTTP_CODE_REDIRECT,
201 self.assertEqual(response.status_code, HTTP_CODE_REDIRECT,
203 msg='Posting new message failed: got code ' +
202 msg='Posting new message failed: got code ' +
204 str(response.status_code))
203 str(response.status_code))
205
204
206 self.assertEqual(1, Post.objects.count(),
205 self.assertEqual(1, Post.objects.count(),
207 msg='No posts were created')
206 msg='No posts were created')
208
207
209 client.post(NEW_THREAD_PAGE, {'text': TEST_TEXT,
208 client.post(NEW_THREAD_PAGE, {'text': TEST_TEXT,
210 'tags': invalid_tags})
209 'tags': invalid_tags})
211 self.assertEqual(1, Post.objects.count(), msg='The validation passed '
210 self.assertEqual(1, Post.objects.count(), msg='The validation passed '
212 'where it should fail')
211 'where it should fail')
213
212
214 # Change posting delay so we don't have to wait for 30 seconds or more
213 # Change posting delay so we don't have to wait for 30 seconds or more
215 old_posting_delay = settings.POSTING_DELAY
214 old_posting_delay = settings.POSTING_DELAY
216 # Wait fot the posting delay or we won't be able to post
215 # Wait fot the posting delay or we won't be able to post
217 settings.POSTING_DELAY = 1
216 settings.POSTING_DELAY = 1
218 time.sleep(settings.POSTING_DELAY + 1)
217 time.sleep(settings.POSTING_DELAY + 1)
219 response = client.post(THREAD_PAGE_ONE, {'text': TEST_TEXT,
218 response = client.post(THREAD_PAGE_ONE, {'text': TEST_TEXT,
220 'tags': valid_tags})
219 'tags': valid_tags})
221 self.assertEqual(HTTP_CODE_REDIRECT, response.status_code,
220 self.assertEqual(HTTP_CODE_REDIRECT, response.status_code,
222 msg=u'Posting new message failed: got code ' +
221 msg=u'Posting new message failed: got code ' +
223 str(response.status_code))
222 str(response.status_code))
224 # Restore posting delay
223 # Restore posting delay
225 settings.POSTING_DELAY = old_posting_delay
224 settings.POSTING_DELAY = old_posting_delay
226
225
227 self.assertEqual(2, Post.objects.count(),
226 self.assertEqual(2, Post.objects.count(),
228 msg=u'No posts were created')
227 msg=u'No posts were created')
229
228
230 # Restore captcha setting
229 # Restore captcha setting
231 settings.ENABLE_CAPTCHA = captcha_enabled
230 settings.ENABLE_CAPTCHA = captcha_enabled
232
231
233
232
234 class ViewTest(TestCase):
233 class ViewTest(TestCase):
235
234
236 def test_all_views(self):
235 def test_all_views(self):
237 '''
236 '''
238 Try opening all views defined in ulrs.py that don't need additional
237 Try opening all views defined in ulrs.py that don't need additional
239 parameters
238 parameters
240 '''
239 '''
241
240
242 client = Client()
241 client = Client()
243 for url in urls.urlpatterns:
242 for url in urls.urlpatterns:
244 try:
243 try:
245 view_name = url.name
244 view_name = url.name
246 logger.debug('Testing view %s' % view_name)
245 logger.debug('Testing view %s' % view_name)
247
246
248 try:
247 try:
249 response = client.get(reverse(view_name))
248 response = client.get(reverse(view_name))
250
249
251 self.assertEqual(HTTP_CODE_OK, response.status_code,
250 self.assertEqual(HTTP_CODE_OK, response.status_code,
252 '%s view not opened' % view_name)
251 '%s view not opened' % view_name)
253 except NoReverseMatch:
252 except NoReverseMatch:
254 # This view just needs additional arguments
253 # This view just needs additional arguments
255 pass
254 pass
256 except Exception, e:
255 except Exception, e:
257 self.fail('Got exception %s at %s view' % (e, view_name))
256 self.fail('Got exception %s at %s view' % (e, view_name))
258 except AttributeError:
257 except AttributeError:
259 # This is normal, some views do not have names
258 # This is normal, some views do not have names
260 pass
259 pass
@@ -1,81 +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
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.static import StaticPageView
9 from boards.views.static import StaticPageView
10 from boards.views.post_admin import PostAdminView
10 from boards.views.post_admin import PostAdminView
11
11
12 js_info_dict = {
12 js_info_dict = {
13 'packages': ('boards',),
13 'packages': ('boards',),
14 }
14 }
15
15
16 urlpatterns = patterns('',
16 urlpatterns = patterns('',
17
17
18 # /boards/
18 # /boards/
19 url(r'^$', all_threads.AllThreadsView.as_view(), name='index'),
19 url(r'^$', all_threads.AllThreadsView.as_view(), name='index'),
20 # /boards/page/
20 # /boards/page/
21 url(r'^page/(?P<page>\w+)/$', all_threads.AllThreadsView.as_view(),
21 url(r'^page/(?P<page>\w+)/$', all_threads.AllThreadsView.as_view(),
22 name='index'),
22 name='index'),
23
23
24 # login page
24 # login page
25 url(r'^login/$', login.LoginView.as_view(), name='login'),
25 url(r'^login/$', login.LoginView.as_view(), name='login'),
26
26
27 # /boards/tag/tag_name/
27 # /boards/tag/tag_name/
28 url(r'^tag/(?P<tag_name>\w+)/$', tag_threads.TagView.as_view(),
28 url(r'^tag/(?P<tag_name>\w+)/$', tag_threads.TagView.as_view(),
29 name='tag'),
29 name='tag'),
30 # /boards/tag/tag_id/page/
30 # /boards/tag/tag_id/page/
31 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/$',
31 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/$',
32 tag_threads.TagView.as_view(), name='tag'),
32 tag_threads.TagView.as_view(), name='tag'),
33
33
34 # /boards/thread/
34 # /boards/thread/
35 url(r'^thread/(?P<post_id>\w+)/$', views.thread.ThreadView.as_view(),
35 url(r'^thread/(?P<post_id>\w+)/$', views.thread.ThreadView.as_view(),
36 name='thread'),
36 name='thread'),
37 url(r'^thread/(?P<post_id>\w+)/mode/(?P<mode>\w+)/$', views.thread.ThreadView
37 url(r'^thread/(?P<post_id>\w+)/mode/(?P<mode>\w+)/$', views.thread.ThreadView
38 .as_view(), name='thread_mode'),
38 .as_view(), name='thread_mode'),
39
39
40 # /boards/post_admin/
40 # /boards/post_admin/
41 url(r'^post_admin/(?P<post_id>\w+)/$', PostAdminView.as_view(),
41 url(r'^post_admin/(?P<post_id>\w+)/$', PostAdminView.as_view(),
42 name='post_admin'),
42 name='post_admin'),
43
43
44 url(r'^settings/$', settings.SettingsView.as_view(), name='settings'),
44 url(r'^settings/$', settings.SettingsView.as_view(), name='settings'),
45 url(r'^tags/$', all_tags.AllTagsView.as_view(), name='tags'),
45 url(r'^tags/$', all_tags.AllTagsView.as_view(), name='tags'),
46 url(r'^captcha/', include('captcha.urls')),
46 url(r'^captcha/', include('captcha.urls')),
47 url(r'^authors/$', AuthorsView.as_view(), name='authors'),
47 url(r'^authors/$', AuthorsView.as_view(), name='authors'),
48 url(r'^delete/(?P<post_id>\w+)/$', DeletePostView.as_view(),
48 url(r'^delete/(?P<post_id>\w+)/$', DeletePostView.as_view(),
49 name='delete'),
49 name='delete'),
50 url(r'^ban/(?P<post_id>\w+)/$', BanUserView.as_view(), name='ban'),
50 url(r'^ban/(?P<post_id>\w+)/$', BanUserView.as_view(), name='ban'),
51
51
52 url(r'^banned/$', views.banned.BannedView.as_view(), name='banned'),
52 url(r'^banned/$', views.banned.BannedView.as_view(), name='banned'),
53 url(r'^staticpage/(?P<name>\w+)/$', StaticPageView.as_view(),
53 url(r'^staticpage/(?P<name>\w+)/$', StaticPageView.as_view(),
54 name='staticpage'),
54 name='staticpage'),
55
55
56 # RSS feeds
56 # RSS feeds
57 url(r'^rss/$', AllThreadsFeed()),
57 url(r'^rss/$', AllThreadsFeed()),
58 url(r'^page/(?P<page>\w+)/rss/$', AllThreadsFeed()),
58 url(r'^page/(?P<page>\w+)/rss/$', AllThreadsFeed()),
59 url(r'^tag/(?P<tag_name>\w+)/rss/$', TagThreadsFeed()),
59 url(r'^tag/(?P<tag_name>\w+)/rss/$', TagThreadsFeed()),
60 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/rss/$', TagThreadsFeed()),
60 url(r'^tag/(?P<tag_name>\w+)/page/(?P<page>\w+)/rss/$', TagThreadsFeed()),
61 url(r'^thread/(?P<post_id>\w+)/rss/$', ThreadPostsFeed()),
61 url(r'^thread/(?P<post_id>\w+)/rss/$', ThreadPostsFeed()),
62
62
63 # i18n
63 # i18n
64 url(r'^jsi18n/$', 'boards.views.cached_js_catalog', js_info_dict,
64 url(r'^jsi18n/$', 'boards.views.cached_js_catalog', js_info_dict,
65 name='js_info_dict'),
65 name='js_info_dict'),
66
66
67 # API
67 # API
68 url(r'^api/post/(?P<post_id>\w+)/$', api.get_post, name="get_post"),
68 url(r'^api/post/(?P<post_id>\w+)/$', api.get_post, name="get_post"),
69 url(r'^api/diff_thread/(?P<thread_id>\w+)/(?P<last_update_time>\w+)/$',
69 url(r'^api/diff_thread/(?P<thread_id>\w+)/(?P<last_update_time>\w+)/$',
70 api.api_get_threaddiff, name="get_thread_diff"),
70 api.api_get_threaddiff, name="get_thread_diff"),
71 url(r'^api/threads/(?P<count>\w+)/$', api.api_get_threads,
71 url(r'^api/threads/(?P<count>\w+)/$', api.api_get_threads,
72 name='get_threads'),
72 name='get_threads'),
73 url(r'^api/tags/$', api.api_get_tags, name='get_tags'),
73 url(r'^api/tags/$', api.api_get_tags, name='get_tags'),
74 url(r'^api/thread/(?P<opening_post_id>\w+)/$', api.api_get_thread_posts,
74 url(r'^api/thread/(?P<opening_post_id>\w+)/$', api.api_get_thread_posts,
75 name='get_thread'),
75 name='get_thread'),
76 url(r'^api/add_post/(?P<opening_post_id>\w+)/$', api.api_add_post,
76 url(r'^api/add_post/(?P<opening_post_id>\w+)/$', api.api_add_post,
77 name='add_post'),
77 name='add_post'),
78 url(r'api/get_tag_popularity/(?P<tag_name>\w+)$', api.get_tag_popularity,
78 url(r'^api/get_tag_popularity/(?P<tag_name>\w+)$', api.get_tag_popularity,
79 name='get_tag_popularity'),
79 name='get_tag_popularity'),
80
80
81 # Search
82 url(r'^search/', include('haystack.urls')),
83
81 )
84 )
@@ -1,80 +1,129 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 from django.utils import timezone
5 from django.utils import timezone
5
6
6 from neboard import settings
7 from neboard import settings
8 from boards.models import Post, User
9 from boards.models.post import SETTING_MODERATE
10 from boards.models.user import RANK_USER
7 import time
11 import time
12 import neboard
8
13
9
14
10 KEY_CAPTCHA_FAILS = 'key_captcha_fails'
15 KEY_CAPTCHA_FAILS = 'key_captcha_fails'
11 KEY_CAPTCHA_DELAY_TIME = 'key_captcha_delay_time'
16 KEY_CAPTCHA_DELAY_TIME = 'key_captcha_delay_time'
12 KEY_CAPTCHA_LAST_ACTIVITY = 'key_captcha_last_activity'
17 KEY_CAPTCHA_LAST_ACTIVITY = 'key_captcha_last_activity'
13
18
14
19
15 def need_include_captcha(request):
20 def need_include_captcha(request):
16 """
21 """
17 Check if request is made by a user.
22 Check if request is made by a user.
18 It contains rules which check for bots.
23 It contains rules which check for bots.
19 """
24 """
20
25
21 if not settings.ENABLE_CAPTCHA:
26 if not settings.ENABLE_CAPTCHA:
22 return False
27 return False
23
28
24 enable_captcha = False
29 enable_captcha = False
25
30
26 #newcomer
31 #newcomer
27 if KEY_CAPTCHA_LAST_ACTIVITY not in request.session:
32 if KEY_CAPTCHA_LAST_ACTIVITY not in request.session:
28 return settings.ENABLE_CAPTCHA
33 return settings.ENABLE_CAPTCHA
29
34
30 last_activity = request.session[KEY_CAPTCHA_LAST_ACTIVITY]
35 last_activity = request.session[KEY_CAPTCHA_LAST_ACTIVITY]
31 current_delay = int(time.time()) - last_activity
36 current_delay = int(time.time()) - last_activity
32
37
33 delay_time = (request.session[KEY_CAPTCHA_DELAY_TIME]
38 delay_time = (request.session[KEY_CAPTCHA_DELAY_TIME]
34 if KEY_CAPTCHA_DELAY_TIME in request.session
39 if KEY_CAPTCHA_DELAY_TIME in request.session
35 else settings.CAPTCHA_DEFAULT_SAFE_TIME)
40 else settings.CAPTCHA_DEFAULT_SAFE_TIME)
36
41
37 if current_delay < delay_time:
42 if current_delay < delay_time:
38 enable_captcha = True
43 enable_captcha = True
39
44
40 print 'ENABLING' + str(enable_captcha)
45 print 'ENABLING' + str(enable_captcha)
41
46
42 return enable_captcha
47 return enable_captcha
43
48
44
49
45 def update_captcha_access(request, passed):
50 def update_captcha_access(request, passed):
46 """
51 """
47 Update captcha fields.
52 Update captcha fields.
48 It will reduce delay time if user passed captcha verification and
53 It will reduce delay time if user passed captcha verification and
49 it will increase it otherwise.
54 it will increase it otherwise.
50 """
55 """
51 session = request.session
56 session = request.session
52
57
53 delay_time = (request.session[KEY_CAPTCHA_DELAY_TIME]
58 delay_time = (request.session[KEY_CAPTCHA_DELAY_TIME]
54 if KEY_CAPTCHA_DELAY_TIME in request.session
59 if KEY_CAPTCHA_DELAY_TIME in request.session
55 else settings.CAPTCHA_DEFAULT_SAFE_TIME)
60 else settings.CAPTCHA_DEFAULT_SAFE_TIME)
56
61
57 print "DELAY TIME = " + str(delay_time)
62 print "DELAY TIME = " + str(delay_time)
58
63
59 if passed:
64 if passed:
60 delay_time -= 2 if delay_time >= 7 else 5
65 delay_time -= 2 if delay_time >= 7 else 5
61 else:
66 else:
62 delay_time += 10
67 delay_time += 10
63
68
64 session[KEY_CAPTCHA_LAST_ACTIVITY] = int(time.time())
69 session[KEY_CAPTCHA_LAST_ACTIVITY] = int(time.time())
65 session[KEY_CAPTCHA_DELAY_TIME] = delay_time
70 session[KEY_CAPTCHA_DELAY_TIME] = delay_time
66
71
67
72
68 def get_client_ip(request):
73 def get_client_ip(request):
69 x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
74 x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
70 if x_forwarded_for:
75 if x_forwarded_for:
71 ip = x_forwarded_for.split(',')[-1].strip()
76 ip = x_forwarded_for.split(',')[-1].strip()
72 else:
77 else:
73 ip = request.META.get('REMOTE_ADDR')
78 ip = request.META.get('REMOTE_ADDR')
74 return ip
79 return ip
75
80
76
81
77 def datetime_to_epoch(datetime):
82 def datetime_to_epoch(datetime):
78 return int(time.mktime(timezone.localtime(
83 return int(time.mktime(timezone.localtime(
79 datetime,timezone.get_current_timezone()).timetuple())
84 datetime,timezone.get_current_timezone()).timetuple())
80 * 1000000 + datetime.microsecond) No newline at end of file
85 * 1000000 + datetime.microsecond)
86
87
88 def get_user(request):
89 """
90 Get current user from the session. If the user does not exist, create
91 a new one.
92 """
93
94 session = request.session
95 if not 'user_id' in session:
96 request.session.save()
97
98 md5 = hashlib.md5()
99 md5.update(session.session_key)
100 new_id = md5.hexdigest()
101
102 while User.objects.filter(user_id=new_id).exists():
103 md5.update(str(timezone.now()))
104 new_id = md5.hexdigest()
105
106 time_now = timezone.now()
107 user = User.objects.create(user_id=new_id, rank=RANK_USER,
108 registration_time=time_now)
109
110 session['user_id'] = user.id
111 else:
112 user = User.objects.select_related('fav_tags').get(
113 id=session['user_id'])
114
115 return user
116
117
118 def get_theme(request, user=None):
119 """
120 Get user's CSS theme
121 """
122
123 if not user:
124 user = get_user(request)
125 theme = user.get_setting('theme')
126 if not theme:
127 theme = neboard.settings.DEFAULT_THEME
128
129 return theme No newline at end of file
@@ -1,12 +1,13 b''
1 from django.shortcuts import render
1 from django.shortcuts import render
2
2
3 from boards.views.base import BaseBoardView
3 from boards.views.base import BaseBoardView
4 from boards.models.tag import Tag
4 from boards.models.tag import Tag
5
5
6
6 class AllTagsView(BaseBoardView):
7 class AllTagsView(BaseBoardView):
7
8
8 def get(self, request):
9 def get(self, request):
9 context = self.get_context_data(request=request)
10 context = self.get_context_data(request=request)
10 context['all_tags'] = Tag.objects.get_not_empty_tags()
11 context['all_tags'] = Tag.objects.get_not_empty_tags()
11
12
12 return render(request, 'boards/tags.html', context)
13 return render(request, 'boards/tags.html', context)
@@ -1,134 +1,134 b''
1 import string
1 import string
2
2
3 from django.core.urlresolvers import reverse
4 from django.db import transaction
3 from django.db import transaction
5 from django.shortcuts import render, redirect
4 from django.shortcuts import render, redirect
6
5
7 from boards import utils
6 from boards import utils
8 from boards.abstracts.paginator import get_paginator
7 from boards.abstracts.paginator import get_paginator
9 from boards.forms import ThreadForm, PlainErrorList
8 from boards.forms import ThreadForm, PlainErrorList
10 from boards.models import Post, Thread, Ban, Tag
9 from boards.models import Post, Thread, Ban, Tag
11 from boards.views.banned import BannedView
10 from boards.views.banned import BannedView
12 from boards.views.base import BaseBoardView, PARAMETER_FORM
11 from boards.views.base import BaseBoardView, PARAMETER_FORM
13 from boards.views.posting_mixin import PostMixin
12 from boards.views.posting_mixin import PostMixin
14 import neboard
13 import neboard
15
14
16 TAG_DELIMITER = ' '
15 TAG_DELIMITER = ' '
17
16
18 PARAMETER_CURRENT_PAGE = 'current_page'
17 PARAMETER_CURRENT_PAGE = 'current_page'
19 PARAMETER_PAGINATOR = 'paginator'
18 PARAMETER_PAGINATOR = 'paginator'
20 PARAMETER_THREADS = 'threads'
19 PARAMETER_THREADS = 'threads'
21
20
22 TEMPLATE = 'boards/posting_general.html'
21 TEMPLATE = 'boards/posting_general.html'
23 DEFAULT_PAGE = 1
22 DEFAULT_PAGE = 1
24
23
25
24
26 class AllThreadsView(PostMixin, BaseBoardView):
25 class AllThreadsView(PostMixin, BaseBoardView):
27
26
28 user = None
27 user = None
29
28
30 def get(self, request, page=DEFAULT_PAGE, form=None):
29 def get(self, request, page=DEFAULT_PAGE, form=None):
31 context = self.get_context_data(request=request)
30 context = self.get_context_data(request=request)
32
31
33 self.user = context['user']
32 self.user = utils.get_user(request)
34
33
35 if not form:
34 if not form:
36 form = ThreadForm(error_class=PlainErrorList)
35 form = ThreadForm(error_class=PlainErrorList)
37
36
38 paginator = get_paginator(self.get_threads(),
37 paginator = get_paginator(self.get_threads(),
39 neboard.settings.THREADS_PER_PAGE)
38 neboard.settings.THREADS_PER_PAGE)
40 paginator.current_page = int(page)
39 paginator.current_page = int(page)
41
40
42 threads = paginator.page(page).object_list
41 threads = paginator.page(page).object_list
43
42
44 context[PARAMETER_THREADS] = threads
43 context[PARAMETER_THREADS] = threads
45 context[PARAMETER_FORM] = form
44 context[PARAMETER_FORM] = form
46
45
47 self._get_page_context(paginator, context, page)
46 self._get_page_context(paginator, context, page)
48
47
49 return render(request, TEMPLATE, context)
48 return render(request, TEMPLATE, context)
50
49
51 def post(self, request, page=DEFAULT_PAGE):
50 def post(self, request, page=DEFAULT_PAGE):
52 form = ThreadForm(request.POST, request.FILES,
51 form = ThreadForm(request.POST, request.FILES,
53 error_class=PlainErrorList)
52 error_class=PlainErrorList)
54 form.session = request.session
53 form.session = request.session
55
54
56 if form.is_valid():
55 if form.is_valid():
57 return self.create_thread(request, form)
56 return self.create_thread(request, form)
58 if form.need_to_ban:
57 if form.need_to_ban:
59 # Ban user because he is suspected to be a bot
58 # Ban user because he is suspected to be a bot
60 self._ban_current_user(request)
59 self._ban_current_user(request)
61
60
62 return self.get(request, page, form)
61 return self.get(request, page, form)
63
62
64 @staticmethod
63 @staticmethod
65 def _get_page_context(paginator, context, page):
64 def _get_page_context(paginator, context, page):
66 """
65 """
67 Get pagination context variables
66 Get pagination context variables
68 """
67 """
69
68
70 context[PARAMETER_PAGINATOR] = paginator
69 context[PARAMETER_PAGINATOR] = paginator
71 context[PARAMETER_CURRENT_PAGE] = paginator.page(int(page))
70 context[PARAMETER_CURRENT_PAGE] = paginator.page(int(page))
72
71
73 def parse_tags_string(self, tag_strings):
72 @staticmethod
73 def parse_tags_string(tag_strings):
74 """
74 """
75 Parses tag list string and returns tag object list.
75 Parses tag list string and returns tag object list.
76 """
76 """
77
77
78 tags = []
78 tags = []
79
79
80 if tag_strings:
80 if tag_strings:
81 tag_strings = tag_strings.split(TAG_DELIMITER)
81 tag_strings = tag_strings.split(TAG_DELIMITER)
82 for tag_name in tag_strings:
82 for tag_name in tag_strings:
83 tag_name = string.lower(tag_name.strip())
83 tag_name = string.lower(tag_name.strip())
84 if len(tag_name) > 0:
84 if len(tag_name) > 0:
85 tag, created = Tag.objects.get_or_create(name=tag_name)
85 tag, created = Tag.objects.get_or_create(name=tag_name)
86 tags.append(tag)
86 tags.append(tag)
87
87
88 return tags
88 return tags
89
89
90 @transaction.atomic
90 @transaction.atomic
91 def create_thread(self, request, form, html_response=True):
91 def create_thread(self, request, form, html_response=True):
92 """
92 """
93 Creates a new thread with an opening post.
93 Creates a new thread with an opening post.
94 """
94 """
95
95
96 ip = utils.get_client_ip(request)
96 ip = utils.get_client_ip(request)
97 is_banned = Ban.objects.filter(ip=ip).exists()
97 is_banned = Ban.objects.filter(ip=ip).exists()
98
98
99 if is_banned:
99 if is_banned:
100 if html_response:
100 if html_response:
101 return redirect(BannedView().as_view())
101 return redirect(BannedView().as_view())
102 else:
102 else:
103 return
103 return
104
104
105 data = form.cleaned_data
105 data = form.cleaned_data
106
106
107 title = data['title']
107 title = data['title']
108 text = data['text']
108 text = data['text']
109
109
110 text = self._remove_invalid_links(text)
110 text = self._remove_invalid_links(text)
111
111
112 if 'image' in data.keys():
112 if 'image' in data.keys():
113 image = data['image']
113 image = data['image']
114 else:
114 else:
115 image = None
115 image = None
116
116
117 tag_strings = data['tags']
117 tag_strings = data['tags']
118
118
119 tags = self.parse_tags_string(tag_strings)
119 tags = self.parse_tags_string(tag_strings)
120
120
121 post = Post.objects.create_post(title=title, text=text, ip=ip,
121 post = Post.objects.create_post(title=title, text=text, ip=ip,
122 image=image, tags=tags,
122 image=image, tags=tags,
123 user=self._get_user(request))
123 user=utils.get_user(request))
124
124
125 if html_response:
125 if html_response:
126 return redirect(post.get_url())
126 return redirect(post.get_url())
127
127
128 def get_threads(self):
128 def get_threads(self):
129 """
129 """
130 Gets list of threads that will be shown on a page.
130 Gets list of threads that will be shown on a page.
131 """
131 """
132
132
133 return Thread.objects.all().order_by('-bump_time')\
133 return Thread.objects.all().order_by('-bump_time')\
134 .exclude(tags__in=self.user.hidden_tags.all())
134 .exclude(tags__in=self.user.hidden_tags.all())
@@ -1,248 +1,246 b''
1 from datetime import datetime
1 from datetime import datetime
2 import json
2 import json
3 import logging
3 import logging
4 from django.db import transaction
4 from django.db import transaction
5 from django.http import HttpResponse
5 from django.http import HttpResponse
6 from django.shortcuts import get_object_or_404, render
6 from django.shortcuts import get_object_or_404, render
7 from django.template import RequestContext
7 from django.template import RequestContext
8 from django.utils import timezone
8 from django.utils import timezone
9 from django.core import serializers
9 from django.core import serializers
10
10
11 from boards.forms import PostForm, PlainErrorList
11 from boards.forms import PostForm, PlainErrorList
12 from boards.models import Post, Thread, Tag
12 from boards.models import Post, Thread, Tag
13 from boards.utils import datetime_to_epoch
13 from boards.utils import datetime_to_epoch
14 from boards.views.thread import ThreadView
14 from boards.views.thread import ThreadView
15
15
16 __author__ = 'neko259'
16 __author__ = 'neko259'
17
17
18 PARAMETER_TRUNCATED = 'truncated'
18 PARAMETER_TRUNCATED = 'truncated'
19 PARAMETER_TAG = 'tag'
19 PARAMETER_TAG = 'tag'
20 PARAMETER_OFFSET = 'offset'
20 PARAMETER_OFFSET = 'offset'
21 PARAMETER_DIFF_TYPE = 'type'
21 PARAMETER_DIFF_TYPE = 'type'
22
22
23 DIFF_TYPE_HTML = 'html'
23 DIFF_TYPE_HTML = 'html'
24 DIFF_TYPE_JSON = 'json'
24 DIFF_TYPE_JSON = 'json'
25
25
26 STATUS_OK = 'ok'
26 STATUS_OK = 'ok'
27 STATUS_ERROR = 'error'
27 STATUS_ERROR = 'error'
28
28
29 logger = logging.getLogger(__name__)
29 logger = logging.getLogger(__name__)
30
30
31
31
32 @transaction.atomic
32 @transaction.atomic
33 def api_get_threaddiff(request, thread_id, last_update_time):
33 def api_get_threaddiff(request, thread_id, last_update_time):
34 """
34 """
35 Gets posts that were changed or added since time
35 Gets posts that were changed or added since time
36 """
36 """
37
37
38 thread = get_object_or_404(Post, id=thread_id).get_thread()
38 thread = get_object_or_404(Post, id=thread_id).get_thread()
39
39
40 logger.info('Getting thread #%s diff since %s' % (thread_id,
41 last_update_time))
42
43 filter_time = datetime.fromtimestamp(float(last_update_time) / 1000000,
40 filter_time = datetime.fromtimestamp(float(last_update_time) / 1000000,
44 timezone.get_current_timezone())
41 timezone.get_current_timezone())
45
42
46 json_data = {
43 json_data = {
47 'added': [],
44 'added': [],
48 'updated': [],
45 'updated': [],
49 'last_update': None,
46 'last_update': None,
50 }
47 }
51 added_posts = Post.objects.filter(thread_new=thread,
48 added_posts = Post.objects.filter(thread_new=thread,
52 pub_time__gt=filter_time) \
49 pub_time__gt=filter_time) \
53 .order_by('pub_time')
50 .order_by('pub_time')
54 updated_posts = Post.objects.filter(thread_new=thread,
51 updated_posts = Post.objects.filter(thread_new=thread,
55 pub_time__lte=filter_time,
52 pub_time__lte=filter_time,
56 last_edit_time__gt=filter_time)
53 last_edit_time__gt=filter_time)
57
54
58 diff_type = DIFF_TYPE_HTML
55 diff_type = DIFF_TYPE_HTML
59 if PARAMETER_DIFF_TYPE in request.GET:
56 if PARAMETER_DIFF_TYPE in request.GET:
60 diff_type = request.GET[PARAMETER_DIFF_TYPE]
57 diff_type = request.GET[PARAMETER_DIFF_TYPE]
61
58
62 for post in added_posts:
59 for post in added_posts:
63 json_data['added'].append(_get_post_data(post.id, diff_type, request))
60 json_data['added'].append(_get_post_data(post.id, diff_type, request))
64 for post in updated_posts:
61 for post in updated_posts:
65 json_data['updated'].append(_get_post_data(post.id, diff_type, request))
62 json_data['updated'].append(_get_post_data(post.id, diff_type, request))
66 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
63 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
67
64
68 return HttpResponse(content=json.dumps(json_data))
65 return HttpResponse(content=json.dumps(json_data))
69
66
70
67
71 def api_add_post(request, opening_post_id):
68 def api_add_post(request, opening_post_id):
72 """
69 """
73 Adds a post and return the JSON response for it
70 Adds a post and return the JSON response for it
74 """
71 """
75
72
76 opening_post = get_object_or_404(Post, id=opening_post_id)
73 opening_post = get_object_or_404(Post, id=opening_post_id)
77
74
78 logger.info('Adding post via api...')
75 logger.info('Adding post via api...')
79
76
80 status = STATUS_OK
77 status = STATUS_OK
81 errors = []
78 errors = []
82
79
83 if request.method == 'POST':
80 if request.method == 'POST':
84 form = PostForm(request.POST, request.FILES, error_class=PlainErrorList)
81 form = PostForm(request.POST, request.FILES, error_class=PlainErrorList)
85 form.session = request.session
82 form.session = request.session
86
83
87 if form.need_to_ban:
84 if form.need_to_ban:
88 # Ban user because he is suspected to be a bot
85 # Ban user because he is suspected to be a bot
89 # _ban_current_user(request)
86 # _ban_current_user(request)
90 status = STATUS_ERROR
87 status = STATUS_ERROR
91 if form.is_valid():
88 if form.is_valid():
92 post = ThreadView().new_post(request, form, opening_post,
89 post = ThreadView().new_post(request, form, opening_post,
93 html_response=False)
90 html_response=False)
94 if not post:
91 if not post:
95 status = STATUS_ERROR
92 status = STATUS_ERROR
96 else:
93 else:
97 logger.info('Added post #%d via api.' % post.id)
94 logger.info('Added post #%d via api.' % post.id)
98 else:
95 else:
99 status = STATUS_ERROR
96 status = STATUS_ERROR
100 errors = form.as_json_errors()
97 errors = form.as_json_errors()
101
98
102 response = {
99 response = {
103 'status': status,
100 'status': status,
104 'errors': errors,
101 'errors': errors,
105 }
102 }
106
103
107 return HttpResponse(content=json.dumps(response))
104 return HttpResponse(content=json.dumps(response))
108
105
109
106
110 def get_post(request, post_id):
107 def get_post(request, post_id):
111 """
108 """
112 Gets the html of a post. Used for popups. Post can be truncated if used
109 Gets the html of a post. Used for popups. Post can be truncated if used
113 in threads list with 'truncated' get parameter.
110 in threads list with 'truncated' get parameter.
114 """
111 """
115
112
116 logger.info('Getting post #%s' % post_id)
113 logger.info('Getting post #%s' % post_id)
117
114
118 post = get_object_or_404(Post, id=post_id)
115 post = get_object_or_404(Post, id=post_id)
119
116
120 context = RequestContext(request)
117 context = RequestContext(request)
121 context['post'] = post
118 context['post'] = post
122 if PARAMETER_TRUNCATED in request.GET:
119 if PARAMETER_TRUNCATED in request.GET:
123 context[PARAMETER_TRUNCATED] = True
120 context[PARAMETER_TRUNCATED] = True
124
121
125 return render(request, 'boards/api_post.html', context)
122 return render(request, 'boards/api_post.html', context)
126
123
127
124
128 # TODO Test this
125 # TODO Test this
129 def api_get_threads(request, count):
126 def api_get_threads(request, count):
130 """
127 """
131 Gets the JSON thread opening posts list.
128 Gets the JSON thread opening posts list.
132 Parameters that can be used for filtering:
129 Parameters that can be used for filtering:
133 tag, offset (from which thread to get results)
130 tag, offset (from which thread to get results)
134 """
131 """
135
132
136 if PARAMETER_TAG in request.GET:
133 if PARAMETER_TAG in request.GET:
137 tag_name = request.GET[PARAMETER_TAG]
134 tag_name = request.GET[PARAMETER_TAG]
138 if tag_name is not None:
135 if tag_name is not None:
139 tag = get_object_or_404(Tag, name=tag_name)
136 tag = get_object_or_404(Tag, name=tag_name)
140 threads = tag.threads.filter(archived=False)
137 threads = tag.threads.filter(archived=False)
141 else:
138 else:
142 threads = Thread.objects.filter(archived=False)
139 threads = Thread.objects.filter(archived=False)
143
140
144 if PARAMETER_OFFSET in request.GET:
141 if PARAMETER_OFFSET in request.GET:
145 offset = request.GET[PARAMETER_OFFSET]
142 offset = request.GET[PARAMETER_OFFSET]
146 offset = int(offset) if offset is not None else 0
143 offset = int(offset) if offset is not None else 0
147 else:
144 else:
148 offset = 0
145 offset = 0
149
146
150 threads = threads.order_by('-bump_time')
147 threads = threads.order_by('-bump_time')
151 threads = threads[offset:offset + int(count)]
148 threads = threads[offset:offset + int(count)]
152
149
153 opening_posts = []
150 opening_posts = []
154 for thread in threads:
151 for thread in threads:
155 opening_post = thread.get_opening_post()
152 opening_post = thread.get_opening_post()
156
153
157 # TODO Add tags, replies and images count
154 # TODO Add tags, replies and images count
158 opening_posts.append(_get_post_data(opening_post.id,
155 opening_posts.append(_get_post_data(opening_post.id,
159 include_last_update=True))
156 include_last_update=True))
160
157
161 return HttpResponse(content=json.dumps(opening_posts))
158 return HttpResponse(content=json.dumps(opening_posts))
162
159
163
160
164 # TODO Test this
161 # TODO Test this
165 def api_get_tags(request):
162 def api_get_tags(request):
166 """
163 """
167 Gets all tags or user tags.
164 Gets all tags or user tags.
168 """
165 """
169
166
170 # TODO Get favorite tags for the given user ID
167 # TODO Get favorite tags for the given user ID
171
168
172 tags = Tag.objects.get_not_empty_tags()
169 tags = Tag.objects.get_not_empty_tags()
173 tag_names = []
170 tag_names = []
174 for tag in tags:
171 for tag in tags:
175 tag_names.append(tag.name)
172 tag_names.append(tag.name)
176
173
177 return HttpResponse(content=json.dumps(tag_names))
174 return HttpResponse(content=json.dumps(tag_names))
178
175
179
176
180 # TODO The result can be cached by the thread last update time
177 # TODO The result can be cached by the thread last update time
181 # TODO Test this
178 # TODO Test this
182 def api_get_thread_posts(request, opening_post_id):
179 def api_get_thread_posts(request, opening_post_id):
183 """
180 """
184 Gets the JSON array of thread posts
181 Gets the JSON array of thread posts
185 """
182 """
186
183
187 opening_post = get_object_or_404(Post, id=opening_post_id)
184 opening_post = get_object_or_404(Post, id=opening_post_id)
188 thread = opening_post.get_thread()
185 thread = opening_post.get_thread()
189 posts = thread.get_replies()
186 posts = thread.get_replies()
190
187
191 json_data = {
188 json_data = {
192 'posts': [],
189 'posts': [],
193 'last_update': None,
190 'last_update': None,
194 }
191 }
195 json_post_list = []
192 json_post_list = []
196
193
197 for post in posts:
194 for post in posts:
198 json_post_list.append(_get_post_data(post.id))
195 json_post_list.append(_get_post_data(post.id))
199 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
196 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
200 json_data['posts'] = json_post_list
197 json_data['posts'] = json_post_list
201
198
202 return HttpResponse(content=json.dumps(json_data))
199 return HttpResponse(content=json.dumps(json_data))
203
200
204
201
205 def api_get_post(request, post_id):
202 def api_get_post(request, post_id):
206 """
203 """
207 Gets the JSON of a post. This can be
204 Gets the JSON of a post. This can be
208 used as and API for external clients.
205 used as and API for external clients.
209 """
206 """
210
207
211 post = get_object_or_404(Post, id=post_id)
208 post = get_object_or_404(Post, id=post_id)
212
209
213 json = serializers.serialize("json", [post], fields=(
210 json = serializers.serialize("json", [post], fields=(
214 "pub_time", "_text_rendered", "title", "text", "image",
211 "pub_time", "_text_rendered", "title", "text", "image",
215 "image_width", "image_height", "replies", "tags"
212 "image_width", "image_height", "replies", "tags"
216 ))
213 ))
217
214
218 return HttpResponse(content=json)
215 return HttpResponse(content=json)
219
216
220
217
221 def get_tag_popularity(request, tag_name):
218 def get_tag_popularity(request, tag_name):
222 tag = get_object_or_404(Tag, name=tag_name)
219 tag = get_object_or_404(Tag, name=tag_name)
223
220
224 json_data = []
221 json_data = []
225 json_data['popularity'] = tag.get_popularity()
222 json_data['popularity'] = tag.get_popularity()
226
223
227 return HttpResponse(content=json.dumps(json_data))
224 return HttpResponse(content=json.dumps(json_data))
228
225
229
226
230 # TODO Add pub time and replies
227 # TODO Add pub time and replies
231 def _get_post_data(post_id, format_type=DIFF_TYPE_JSON, request=None,
228 def _get_post_data(post_id, format_type=DIFF_TYPE_JSON, request=None,
232 include_last_update=False):
229 include_last_update=False):
233 if format_type == DIFF_TYPE_HTML:
230 if format_type == DIFF_TYPE_HTML:
234 return get_post(request, post_id).content.strip()
231 return get_post(request, post_id).content.strip()
235 elif format_type == DIFF_TYPE_JSON:
232 elif format_type == DIFF_TYPE_JSON:
236 post = get_object_or_404(Post, id=post_id)
233 post = get_object_or_404(Post, id=post_id)
237 post_json = {
234 post_json = {
238 'id': post.id,
235 'id': post.id,
239 'title': post.title,
236 'title': post.title,
240 'text': post.text.rendered,
237 'text': post.text.rendered,
241 }
238 }
242 if post.image:
239 if post.images.exists():
243 post_json['image'] = post.image.url
240 post_image = post.get_first_image()
244 post_json['image_preview'] = post.image.url_200x150
241 post_json['image'] = post_image.image.url
242 post_json['image_preview'] = post_image.image.url_200x150
245 if include_last_update:
243 if include_last_update:
246 post_json['bump_time'] = datetime_to_epoch(
244 post_json['bump_time'] = datetime_to_epoch(
247 post.thread_new.bump_time)
245 post.thread_new.bump_time)
248 return post_json
246 return post_json
@@ -1,23 +1,24 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
3
4
4 from boards.views.base import BaseBoardView
5 from boards.views.base import BaseBoardView
5 from boards.models import Post, Ban
6 from boards.models import Post, Ban
6 from boards.views.mixins import RedirectNextMixin
7 from boards.views.mixins import RedirectNextMixin
7
8
8
9
9 class BanUserView(BaseBoardView, RedirectNextMixin):
10 class BanUserView(BaseBoardView, RedirectNextMixin):
10
11
11 @transaction.atomic
12 @transaction.atomic
12 def get(self, request, post_id):
13 def get(self, request, post_id):
13 user = self._get_user(request)
14 user = utils.get_user(request)
14 post = get_object_or_404(Post, id=post_id)
15 post = get_object_or_404(Post, id=post_id)
15
16
16 if user.is_moderator():
17 if user.is_moderator():
17 # TODO Show confirmation page before ban
18 # TODO Show confirmation page before ban
18 ban, created = Ban.objects.get_or_create(ip=post.poster_ip)
19 ban, created = Ban.objects.get_or_create(ip=post.poster_ip)
19 if created:
20 if created:
20 ban.reason = 'Banned for post ' + str(post_id)
21 ban.reason = 'Banned for post ' + str(post_id)
21 ban.save()
22 ban.save()
22
23
23 return self.redirect_to_next(request)
24 return self.redirect_to_next(request)
@@ -1,127 +1,35 b''
1 from datetime import datetime, timedelta
2 import hashlib
3 from django.db import transaction
1 from django.db import transaction
4 from django.db.models import Count
5 from django.template import RequestContext
2 from django.template import RequestContext
6 from django.utils import timezone
7 from django.views.generic import View
3 from django.views.generic import View
4
8 from boards import utils
5 from boards import utils
9 from boards.models import User, Post
6 from boards.models.user import Ban
10 from boards.models.post import SETTING_MODERATE
7
11 from boards.models.user import RANK_USER, Ban
12 import neboard
13
8
14 BAN_REASON_SPAM = 'Autoban: spam bot'
9 BAN_REASON_SPAM = 'Autoban: spam bot'
15
10
16 OLD_USER_AGE_DAYS = 90
17
18 PARAMETER_FORM = 'form'
11 PARAMETER_FORM = 'form'
19
12
20
13
21 class BaseBoardView(View):
14 class BaseBoardView(View):
22
15
23 def get_context_data(self, **kwargs):
16 def get_context_data(self, **kwargs):
24 request = kwargs['request']
17 request = kwargs['request']
25 context = self._default_context(request)
18 # context = self._default_context(request)
26
27 context['version'] = neboard.settings.VERSION
28 context['site_name'] = neboard.settings.SITE_NAME
29
30 return context
31
32 def _default_context(self, request):
33 """Create context with default values that are used in most views"""
34
35 context = RequestContext(request)
19 context = RequestContext(request)
36
20
37 user = self._get_user(request)
38 context['user'] = user
39 context['tags'] = user.fav_tags.all()
40 context['posts_per_day'] = float(Post.objects.get_posts_per_day())
41
42 theme = self._get_theme(request, user)
43 context['theme'] = theme
44 context['theme_css'] = 'css/' + theme + '/base_page.css'
45
46 # This shows the moderator panel
47 moderate = user.get_setting(SETTING_MODERATE)
48 if moderate == 'True':
49 context['moderator'] = user.is_moderator()
50 else:
51 context['moderator'] = False
52
53 return context
21 return context
54
22
55 def _get_user(self, request):
56 """
57 Get current user from the session. If the user does not exist, create
58 a new one.
59 """
60
61 session = request.session
62 if not 'user_id' in session:
63 request.session.save()
64
65 md5 = hashlib.md5()
66 md5.update(session.session_key)
67 new_id = md5.hexdigest()
68
69 while User.objects.filter(user_id=new_id).exists():
70 md5.update(str(timezone.now()))
71 new_id = md5.hexdigest()
72
73 time_now = timezone.now()
74 user = User.objects.create(user_id=new_id, rank=RANK_USER,
75 registration_time=time_now)
76
77 # TODO Move this to manage.py commands
78 #self._delete_old_users()
79
80 session['user_id'] = user.id
81 else:
82 user = User.objects.select_related('fav_tags').get(
83 id=session['user_id'])
84
85 return user
86
87 def _get_theme(self, request, user=None):
88 """
89 Get user's CSS theme
90 """
91
92 if not user:
93 user = self._get_user(request)
94 theme = user.get_setting('theme')
95 if not theme:
96 theme = neboard.settings.DEFAULT_THEME
97
98 return theme
99
100 def _delete_old_users(self):
101 """
102 Delete users with no favorite tags and posted messages. These can be spam
103 bots or just old user accounts
104 """
105
106 old_registration_date = datetime.now().date() - timedelta(
107 OLD_USER_AGE_DAYS)
108
109 for user in User.objects.annotate(tags_count=Count('fav_tags')).filter(
110 tags_count=0).filter(
111 registration_time__lt=old_registration_date):
112 if not Post.objects.filter(user=user).exists():
113 user.delete()
114
115 @transaction.atomic
23 @transaction.atomic
116 def _ban_current_user(self, request):
24 def _ban_current_user(self, request):
117 """
25 """
118 Add current user to the IP ban list
26 Add current user to the IP ban list
119 """
27 """
120
28
121 ip = utils.get_client_ip(request)
29 ip = utils.get_client_ip(request)
122 ban, created = Ban.objects.get_or_create(ip=ip)
30 ban, created = Ban.objects.get_or_create(ip=ip)
123 if created:
31 if created:
124 ban.can_read = False
32 ban.can_read = False
125 ban.reason = BAN_REASON_SPAM
33 ban.reason = BAN_REASON_SPAM
126 ban.save()
34 ban.save()
127
35
@@ -1,26 +1,27 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
3
4
4 from boards.views.base import BaseBoardView
5 from boards.views.base import BaseBoardView
5 from boards.views.mixins import RedirectNextMixin
6 from boards.views.mixins import RedirectNextMixin
6 from boards.models import Post
7 from boards.models import Post
7
8
8
9
9 class DeletePostView(BaseBoardView, RedirectNextMixin):
10 class DeletePostView(BaseBoardView, RedirectNextMixin):
10
11
11 @transaction.atomic
12 @transaction.atomic
12 def get(self, request, post_id):
13 def get(self, request, post_id):
13 user = self._get_user(request)
14 user = utils.get_user(request)
14 post = get_object_or_404(Post, id=post_id)
15 post = get_object_or_404(Post, id=post_id)
15
16
16 opening_post = post.is_opening()
17 opening_post = post.is_opening()
17
18
18 if user.is_moderator():
19 if user.is_moderator():
19 # TODO Show confirmation page before deletion
20 # TODO Show confirmation page before deletion
20 Post.objects.delete_post(post)
21 Post.objects.delete_post(post)
21
22
22 if not opening_post:
23 if not opening_post:
23 thread = post.thread_new
24 thread = post.thread_new
24 return redirect('thread', post_id=thread.get_opening_post().id)
25 return redirect('thread', post_id=thread.get_opening_post().id)
25 else:
26 else:
26 return self.redirect_to_next(request)
27 return self.redirect_to_next(request)
@@ -1,57 +1,57 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.views.base import BaseBoardView
3 from boards.views.base import BaseBoardView
4 from boards.views.mixins import DispatcherMixin
4 from boards.views.mixins import DispatcherMixin
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.forms import AddTagForm, PlainErrorList
7 from boards.forms import AddTagForm, PlainErrorList
8
8
9 class PostAdminView(BaseBoardView, DispatcherMixin):
9 class PostAdminView(BaseBoardView, DispatcherMixin):
10
10
11 def get(self, request, post_id, form=None):
11 def get(self, request, post_id, form=None):
12 user = self._get_user(request)
12 user = utils.get_user(request)
13 if not user.is_moderator:
13 if not user.is_moderator:
14 redirect('index')
14 redirect('index')
15
15
16 post = get_object_or_404(Post, id=post_id)
16 post = get_object_or_404(Post, id=post_id)
17
17
18 if not form:
18 if not form:
19 dispatch_result = self.dispatch_method(request, post)
19 dispatch_result = self.dispatch_method(request, post)
20 if dispatch_result:
20 if dispatch_result:
21 return dispatch_result
21 return dispatch_result
22 form = AddTagForm()
22 form = AddTagForm()
23
23
24 context = self.get_context_data(request=request)
24 context = self.get_context_data(request=request)
25
25
26 context['post'] = post
26 context['post'] = post
27
27
28 context['tag_form'] = form
28 context['tag_form'] = form
29
29
30 return render(request, 'boards/post_admin.html', context)
30 return render(request, 'boards/post_admin.html', context)
31
31
32 def post(self, request, post_id):
32 def post(self, request, post_id):
33 user = self._get_user(request)
33 user = utils.get_user(request)
34 if not user.is_moderator:
34 if not user.is_moderator:
35 redirect('index')
35 redirect('index')
36
36
37 post = get_object_or_404(Post, id=post_id)
37 post = get_object_or_404(Post, id=post_id)
38 return self.dispatch_method(request, post)
38 return self.dispatch_method(request, post)
39
39
40 def delete_tag(self, request, post):
40 def delete_tag(self, request, post):
41 tag_name = request.GET['tag']
41 tag_name = request.GET['tag']
42 tag = get_object_or_404(Tag, name=tag_name)
42 tag = get_object_or_404(Tag, name=tag_name)
43
43
44 post.remove_tag(tag)
44 post.remove_tag(tag)
45
45
46 return redirect('post_admin', post.id)
46 return redirect('post_admin', post.id)
47
47
48 def add_tag(self, request, post):
48 def add_tag(self, request, post):
49 form = AddTagForm(request.POST, error_class=PlainErrorList)
49 form = AddTagForm(request.POST, error_class=PlainErrorList)
50 if form.is_valid():
50 if form.is_valid():
51 tag_name = form.cleaned_data['tag']
51 tag_name = form.cleaned_data['tag']
52 tag, created = Tag.objects.get_or_create(name=tag_name)
52 tag, created = Tag.objects.get_or_create(name=tag_name)
53
53
54 post.add_tag(tag)
54 post.add_tag(tag)
55 return redirect('post_admin', post.id)
55 return redirect('post_admin', post.id)
56 else:
56 else:
57 return self.get(request, post.id, form)
57 return self.get(request, post.id, form)
@@ -1,52 +1,53 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
3
4
4 from boards.views.base import BaseBoardView, PARAMETER_FORM
5 from boards.views.base import BaseBoardView, PARAMETER_FORM
5 from boards.forms import SettingsForm, ModeratorSettingsForm, PlainErrorList
6 from boards.forms import SettingsForm, ModeratorSettingsForm, PlainErrorList
6 from boards.models.post import SETTING_MODERATE
7 from boards.models.post import SETTING_MODERATE
7
8
8
9
9 class SettingsView(BaseBoardView):
10 class SettingsView(BaseBoardView):
10
11
11 def get(self, request):
12 def get(self, request):
12 context = self.get_context_data(request=request)
13 context = self.get_context_data(request=request)
13 user = context['user']
14 user = utils.get_user(request)
14 is_moderator = user.is_moderator()
15 is_moderator = user.is_moderator()
15
16
16 selected_theme = context['theme']
17 selected_theme = utils.get_theme(request, user)
17
18
18 if is_moderator:
19 if is_moderator:
19 form = ModeratorSettingsForm(initial={
20 form = ModeratorSettingsForm(initial={
20 'theme': selected_theme,
21 'theme': selected_theme,
21 'moderate': context['moderator']
22 'moderate': user.get_setting(SETTING_MODERATE) and \
23 user.is_moderator()
22 }, error_class=PlainErrorList)
24 }, error_class=PlainErrorList)
23 else:
25 else:
24 form = SettingsForm(initial={'theme': selected_theme},
26 form = SettingsForm(initial={'theme': selected_theme},
25 error_class=PlainErrorList)
27 error_class=PlainErrorList)
26
28
27 context[PARAMETER_FORM] = form
29 context[PARAMETER_FORM] = form
28
30
29 return render(request, 'boards/settings.html', context)
31 return render(request, 'boards/settings.html', context)
30
32
31 def post(self, request):
33 def post(self, request):
32 context = self.get_context_data(request=request)
34 user = utils.get_user(request)
33 user = context['user']
34 is_moderator = user.is_moderator()
35 is_moderator = user.is_moderator()
35
36
36 with transaction.atomic():
37 with transaction.atomic():
37 if is_moderator:
38 if is_moderator:
38 form = ModeratorSettingsForm(request.POST,
39 form = ModeratorSettingsForm(request.POST,
39 error_class=PlainErrorList)
40 error_class=PlainErrorList)
40 else:
41 else:
41 form = SettingsForm(request.POST, error_class=PlainErrorList)
42 form = SettingsForm(request.POST, error_class=PlainErrorList)
42
43
43 if form.is_valid():
44 if form.is_valid():
44 selected_theme = form.cleaned_data['theme']
45 selected_theme = form.cleaned_data['theme']
45
46
46 user.save_setting('theme', selected_theme)
47 user.save_setting('theme', selected_theme)
47
48
48 if is_moderator:
49 if is_moderator:
49 moderate = form.cleaned_data['moderate']
50 moderate = form.cleaned_data['moderate']
50 user.save_setting(SETTING_MODERATE, moderate)
51 user.save_setting(SETTING_MODERATE, moderate)
51
52
52 return redirect('settings')
53 return redirect('settings')
@@ -1,13 +1,14 b''
1 from django.shortcuts import render
1 from django.shortcuts import render
2
2
3 from boards.views.base import BaseBoardView
3 from boards.views.base import BaseBoardView
4
4
5
5 class StaticPageView(BaseBoardView):
6 class StaticPageView(BaseBoardView):
6
7
7 def get(self, request, name):
8 def get(self, request, name):
8 """
9 """
9 Show a static page that needs only tags list and a CSS
10 Show a static page that needs only tags list and a CSS
10 """
11 """
11
12
12 context = self.get_context_data(request=request)
13 context = self.get_context_data(request=request)
13 return render(request, 'boards/staticpages/' + name + '.html', context)
14 return render(request, 'boards/staticpages/' + name + '.html', context)
@@ -1,86 +1,87 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 from boards.models import Tag, Post
3 from boards.models import Tag, Post
3 from boards.views.all_threads import AllThreadsView, DEFAULT_PAGE
4 from boards.views.all_threads import AllThreadsView, DEFAULT_PAGE
4 from boards.views.mixins import DispatcherMixin, RedirectNextMixin
5 from boards.views.mixins import DispatcherMixin, RedirectNextMixin
5 from boards.forms import ThreadForm, PlainErrorList
6 from boards.forms import ThreadForm, PlainErrorList
6
7
7 __author__ = 'neko259'
8 __author__ = 'neko259'
8
9
9
10
10 class TagView(AllThreadsView, DispatcherMixin, RedirectNextMixin):
11 class TagView(AllThreadsView, DispatcherMixin, RedirectNextMixin):
11
12
12 tag_name = None
13 tag_name = None
13
14
14 def get_threads(self):
15 def get_threads(self):
15 tag = get_object_or_404(Tag, name=self.tag_name)
16 tag = get_object_or_404(Tag, name=self.tag_name)
16
17
17 return tag.threads.all().order_by('-bump_time')
18 return tag.threads.all().order_by('-bump_time')
18
19
19 def get_context_data(self, **kwargs):
20 def get_context_data(self, **kwargs):
20 context = super(TagView, self).get_context_data(**kwargs)
21 context = super(TagView, self).get_context_data(**kwargs)
21
22
22 tag = get_object_or_404(Tag, name=self.tag_name)
23 tag = get_object_or_404(Tag, name=self.tag_name)
23 context['tag'] = tag
24 context['tag'] = tag
24
25
25 return context
26 return context
26
27
27 def get(self, request, tag_name, page=DEFAULT_PAGE, form=None):
28 def get(self, request, tag_name, page=DEFAULT_PAGE, form=None):
28 self.tag_name = tag_name
29 self.tag_name = tag_name
29
30
30 dispatch_result = self.dispatch_method(request)
31 dispatch_result = self.dispatch_method(request)
31 if dispatch_result:
32 if dispatch_result:
32 return dispatch_result
33 return dispatch_result
33 else:
34 else:
34 return super(TagView, self).get(request, page, form)
35 return super(TagView, self).get(request, page, form)
35
36
36 def post(self, request, tag_name, page=DEFAULT_PAGE):
37 def post(self, request, tag_name, page=DEFAULT_PAGE):
37 form = ThreadForm(request.POST, request.FILES,
38 form = ThreadForm(request.POST, request.FILES,
38 error_class=PlainErrorList)
39 error_class=PlainErrorList)
39 form.session = request.session
40 form.session = request.session
40
41
41 if form.is_valid():
42 if form.is_valid():
42 return self.create_thread(request, form)
43 return self.create_thread(request, form)
43 if form.need_to_ban:
44 if form.need_to_ban:
44 # Ban user because he is suspected to be a bot
45 # Ban user because he is suspected to be a bot
45 self._ban_current_user(request)
46 self._ban_current_user(request)
46
47
47 return self.get(request, tag_name, page, form)
48 return self.get(request, tag_name, page, form)
48
49
49 def subscribe(self, request):
50 def subscribe(self, request):
50 user = self._get_user(request)
51 user = utils.get_user(request)
51 tag = get_object_or_404(Tag, name=self.tag_name)
52 tag = get_object_or_404(Tag, name=self.tag_name)
52
53
53 if not tag in user.fav_tags.all():
54 if not tag in user.fav_tags.all():
54 user.add_tag(tag)
55 user.add_tag(tag)
55
56
56 return self.redirect_to_next(request)
57 return self.redirect_to_next(request)
57
58
58 def unsubscribe(self, request):
59 def unsubscribe(self, request):
59 user = self._get_user(request)
60 user = utils.get_user(request)
60 tag = get_object_or_404(Tag, name=self.tag_name)
61 tag = get_object_or_404(Tag, name=self.tag_name)
61
62
62 if tag in user.fav_tags.all():
63 if tag in user.fav_tags.all():
63 user.remove_tag(tag)
64 user.remove_tag(tag)
64
65
65 return self.redirect_to_next(request)
66 return self.redirect_to_next(request)
66
67
67 def hide(self, request):
68 def hide(self, request):
68 """
69 """
69 Adds tag to user's hidden tags. Threads with this tag will not be
70 Adds tag to user's hidden tags. Threads with this tag will not be
70 shown.
71 shown.
71 """
72 """
72
73
73 user = self._get_user(request)
74 user = utils.get_user(request)
74 tag = get_object_or_404(Tag, name=self.tag_name)
75 tag = get_object_or_404(Tag, name=self.tag_name)
75
76
76 user.hide_tag(tag)
77 user.hide_tag(tag)
77
78
78 def unhide(self, request):
79 def unhide(self, request):
79 """
80 """
80 Removed tag from user's hidden tags.
81 Removed tag from user's hidden tags.
81 """
82 """
82
83
83 user = self._get_user(request)
84 user = utils.get_user(request)
84 tag = get_object_or_404(Tag, name=self.tag_name)
85 tag = get_object_or_404(Tag, name=self.tag_name)
85
86
86 user.unhide_tag(tag)
87 user.unhide_tag(tag)
@@ -1,129 +1,133 b''
1 import string
2 from django.core.urlresolvers import reverse
1 from django.core.urlresolvers import reverse
3 from django.db import transaction
2 from django.db import transaction
4 from django.http import Http404
3 from django.http import Http404
5 from django.shortcuts import get_object_or_404, render, redirect
4 from django.shortcuts import get_object_or_404, render, redirect
6 from django.views.generic.edit import FormMixin
5 from django.views.generic.edit import FormMixin
6
7 from boards import utils
7 from boards import utils
8 from boards.forms import PostForm, PlainErrorList
8 from boards.forms import PostForm, PlainErrorList
9 from boards.models import Post, Ban, Tag
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, PARAMETER_FORM
11 from boards.views.base import BaseBoardView, PARAMETER_FORM
12 from boards.views.posting_mixin import PostMixin
12 from boards.views.posting_mixin import PostMixin
13 import neboard
13 import neboard
14
14
15
15 MODE_GALLERY = 'gallery'
16 MODE_GALLERY = 'gallery'
16 MODE_NORMAL = 'normal'
17 MODE_NORMAL = 'normal'
17
18
18 PARAMETER_MAX_REPLIES = 'max_replies'
19 PARAMETER_MAX_REPLIES = 'max_replies'
19 PARAMETER_THREAD = 'thread'
20 PARAMETER_THREAD = 'thread'
20 PARAMETER_BUMPABLE = 'bumpable'
21 PARAMETER_BUMPABLE = 'bumpable'
21
22
22
23
23 class ThreadView(BaseBoardView, PostMixin, FormMixin):
24 class ThreadView(BaseBoardView, PostMixin, FormMixin):
24
25
25 def get(self, request, post_id, mode=MODE_NORMAL, form=None):
26 def get(self, request, post_id, mode=MODE_NORMAL, form=None):
26 opening_post = Post.objects.filter(id=post_id).only('thread_new')[0]
27 try:
28 opening_post = Post.objects.filter(id=post_id).only('thread_new')[0]
29 except IndexError:
30 raise Http404
27
31
28 # If this is not OP, don't show it as it is
32 # If this is not OP, don't show it as it is
29 if not opening_post or not opening_post.is_opening():
33 if not opening_post or not opening_post.is_opening():
30 raise Http404
34 raise Http404
31
35
32 if not form:
36 if not form:
33 form = PostForm(error_class=PlainErrorList)
37 form = PostForm(error_class=PlainErrorList)
34
38
35 thread_to_show = opening_post.get_thread()
39 thread_to_show = opening_post.get_thread()
36
40
37 context = self.get_context_data(request=request)
41 context = self.get_context_data(request=request)
38
42
39 context[PARAMETER_FORM] = form
43 context[PARAMETER_FORM] = form
40 context["last_update"] = utils.datetime_to_epoch(
44 context["last_update"] = utils.datetime_to_epoch(
41 thread_to_show.last_edit_time)
45 thread_to_show.last_edit_time)
42 context[PARAMETER_THREAD] = thread_to_show
46 context[PARAMETER_THREAD] = thread_to_show
43 context[PARAMETER_MAX_REPLIES] = neboard.settings.MAX_POSTS_PER_THREAD
47 context[PARAMETER_MAX_REPLIES] = neboard.settings.MAX_POSTS_PER_THREAD
44
48
45 if MODE_NORMAL == mode:
49 if MODE_NORMAL == mode:
46 context[PARAMETER_BUMPABLE] = thread_to_show.can_bump()
50 context[PARAMETER_BUMPABLE] = thread_to_show.can_bump()
47 if context[PARAMETER_BUMPABLE]:
51 if context[PARAMETER_BUMPABLE]:
48 context['posts_left'] = neboard.settings.MAX_POSTS_PER_THREAD \
52 context['posts_left'] = neboard.settings.MAX_POSTS_PER_THREAD \
49 - thread_to_show.get_reply_count()
53 - thread_to_show.get_reply_count()
50 context['bumplimit_progress'] = str(
54 context['bumplimit_progress'] = str(
51 float(context['posts_left']) /
55 float(context['posts_left']) /
52 neboard.settings.MAX_POSTS_PER_THREAD * 100)
56 neboard.settings.MAX_POSTS_PER_THREAD * 100)
53
57
54 context['opening_post'] = opening_post
58 context['opening_post'] = opening_post
55
59
56 document = 'boards/thread.html'
60 document = 'boards/thread.html'
57 elif MODE_GALLERY == mode:
61 elif MODE_GALLERY == mode:
58 posts = thread_to_show.get_replies(view_fields_only=True)
62 context['posts'] = thread_to_show.get_replies_with_images(
59 context['posts'] = posts.filter(image_width__gt=0)
63 view_fields_only=True)
60
64
61 document = 'boards/thread_gallery.html'
65 document = 'boards/thread_gallery.html'
62 else:
66 else:
63 raise Http404
67 raise Http404
64
68
65 return render(request, document, context)
69 return render(request, document, context)
66
70
67 def post(self, request, post_id, mode=MODE_NORMAL):
71 def post(self, request, post_id, mode=MODE_NORMAL):
68 opening_post = get_object_or_404(Post, id=post_id)
72 opening_post = get_object_or_404(Post, id=post_id)
69
73
70 # If this is not OP, don't show it as it is
74 # If this is not OP, don't show it as it is
71 if not opening_post.is_opening():
75 if not opening_post.is_opening():
72 raise Http404
76 raise Http404
73
77
74 if not opening_post.get_thread().archived:
78 if not opening_post.get_thread().archived:
75 form = PostForm(request.POST, request.FILES,
79 form = PostForm(request.POST, request.FILES,
76 error_class=PlainErrorList)
80 error_class=PlainErrorList)
77 form.session = request.session
81 form.session = request.session
78
82
79 if form.is_valid():
83 if form.is_valid():
80 return self.new_post(request, form, opening_post)
84 return self.new_post(request, form, opening_post)
81 if form.need_to_ban:
85 if form.need_to_ban:
82 # Ban user because he is suspected to be a bot
86 # Ban user because he is suspected to be a bot
83 self._ban_current_user(request)
87 self._ban_current_user(request)
84
88
85 return self.get(request, post_id, mode, form)
89 return self.get(request, post_id, mode, form)
86
90
87 @transaction.atomic
91 @transaction.atomic
88 def new_post(self, request, form, opening_post=None, html_response=True):
92 def new_post(self, request, form, opening_post=None, html_response=True):
89 """Add a new post (in thread or as a reply)."""
93 """Add a new post (in thread or as a reply)."""
90
94
91 ip = utils.get_client_ip(request)
95 ip = utils.get_client_ip(request)
92 is_banned = Ban.objects.filter(ip=ip).exists()
96 is_banned = Ban.objects.filter(ip=ip).exists()
93
97
94 if is_banned:
98 if is_banned:
95 if html_response:
99 if html_response:
96 return redirect(BannedView().as_view())
100 return redirect(BannedView().as_view())
97 else:
101 else:
98 return None
102 return None
99
103
100 data = form.cleaned_data
104 data = form.cleaned_data
101
105
102 title = data['title']
106 title = data['title']
103 text = data['text']
107 text = data['text']
104
108
105 text = self._remove_invalid_links(text)
109 text = self._remove_invalid_links(text)
106
110
107 if 'image' in data.keys():
111 if 'image' in data.keys():
108 image = data['image']
112 image = data['image']
109 else:
113 else:
110 image = None
114 image = None
111
115
112 tags = []
116 tags = []
113
117
114 post_thread = opening_post.get_thread()
118 post_thread = opening_post.get_thread()
115
119
116 post = Post.objects.create_post(title=title, text=text, ip=ip,
120 post = Post.objects.create_post(title=title, text=text, ip=ip,
117 thread=post_thread, image=image,
121 thread=post_thread, image=image,
118 tags=tags,
122 tags=tags,
119 user=self._get_user(request))
123 user=utils.get_user(request))
120
124
121 thread_to_show = (opening_post.id if opening_post else post.id)
125 thread_to_show = (opening_post.id if opening_post else post.id)
122
126
123 if html_response:
127 if html_response:
124 if opening_post:
128 if opening_post:
125 return redirect(reverse(
129 return redirect(reverse(
126 'thread',
130 'thread',
127 kwargs={'post_id': thread_to_show}) + '#' + str(post.id))
131 kwargs={'post_id': thread_to_show}) + '#' + str(post.id))
128 else:
132 else:
129 return post
133 return post
@@ -1,28 +1,37 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
30 # 1.8 Kara
31 * [CODE] Removed thread update logging
32 * [CODE] Refactored compact form. Now it uses the same one form and moves
33 elements instead of swapping them
34 * [CODE] Moved image to a separate model. This will allow to add multiple
35 images to a post
36 * Added search over posts and tags
37 * [ADMIN] Command to remove empty users
@@ -1,674 +1,674 b''
1 GNU GENERAL PUBLIC LICENSE
1 GNU GENERAL PUBLIC LICENSE
2 Version 3, 29 June 2007
2 Version 3, 29 June 2007
3
3
4 Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
4 Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
5 Everyone is permitted to copy and distribute verbatim copies
5 Everyone is permitted to copy and distribute verbatim copies
6 of this license document, but changing it is not allowed.
6 of this license document, but changing it is not allowed.
7
7
8 Preamble
8 Preamble
9
9
10 The GNU General Public License is a free, copyleft license for
10 The GNU General Public License is a free, copyleft license for
11 software and other kinds of works.
11 software and other kinds of works.
12
12
13 The licenses for most software and other practical works are designed
13 The licenses for most software and other practical works are designed
14 to take away your freedom to share and change the works. By contrast,
14 to take away your freedom to share and change the works. By contrast,
15 the GNU General Public License is intended to guarantee your freedom to
15 the GNU General Public License is intended to guarantee your freedom to
16 share and change all versions of a program--to make sure it remains free
16 share and change all versions of a program--to make sure it remains free
17 software for all its users. We, the Free Software Foundation, use the
17 software for all its users. We, the Free Software Foundation, use the
18 GNU General Public License for most of our software; it applies also to
18 GNU General Public License for most of our software; it applies also to
19 any other work released this way by its authors. You can apply it to
19 any other work released this way by its authors. You can apply it to
20 your programs, too.
20 your programs, too.
21
21
22 When we speak of free software, we are referring to freedom, not
22 When we speak of free software, we are referring to freedom, not
23 price. Our General Public Licenses are designed to make sure that you
23 price. Our General Public Licenses are designed to make sure that you
24 have the freedom to distribute copies of free software (and charge for
24 have the freedom to distribute copies of free software (and charge for
25 them if you wish), that you receive source code or can get it if you
25 them if you wish), that you receive source code or can get it if you
26 want it, that you can change the software or use pieces of it in new
26 want it, that you can change the software or use pieces of it in new
27 free programs, and that you know you can do these things.
27 free programs, and that you know you can do these things.
28
28
29 To protect your rights, we need to prevent others from denying you
29 To protect your rights, we need to prevent others from denying you
30 these rights or asking you to surrender the rights. Therefore, you have
30 these rights or asking you to surrender the rights. Therefore, you have
31 certain responsibilities if you distribute copies of the software, or if
31 certain responsibilities if you distribute copies of the software, or if
32 you modify it: responsibilities to respect the freedom of others.
32 you modify it: responsibilities to respect the freedom of others.
33
33
34 For example, if you distribute copies of such a program, whether
34 For example, if you distribute copies of such a program, whether
35 gratis or for a fee, you must pass on to the recipients the same
35 gratis or for a fee, you must pass on to the recipients the same
36 freedoms that you received. You must make sure that they, too, receive
36 freedoms that you received. You must make sure that they, too, receive
37 or can get the source code. And you must show them these terms so they
37 or can get the source code. And you must show them these terms so they
38 know their rights.
38 know their rights.
39
39
40 Developers that use the GNU GPL protect your rights with two steps:
40 Developers that use the GNU GPL protect your rights with two steps:
41 (1) assert copyright on the software, and (2) offer you this License
41 (1) assert copyright on the software, and (2) offer you this License
42 giving you legal permission to copy, distribute and/or modify it.
42 giving you legal permission to copy, distribute and/or modify it.
43
43
44 For the developers' and authors' protection, the GPL clearly explains
44 For the developers' and authors' protection, the GPL clearly explains
45 that there is no warranty for this free software. For both users' and
45 that there is no warranty for this free software. For both users' and
46 authors' sake, the GPL requires that modified versions be marked as
46 authors' sake, the GPL requires that modified versions be marked as
47 changed, so that their problems will not be attributed erroneously to
47 changed, so that their problems will not be attributed erroneously to
48 authors of previous versions.
48 authors of previous versions.
49
49
50 Some devices are designed to deny users access to install or run
50 Some devices are designed to deny users access to install or run
51 modified versions of the software inside them, although the manufacturer
51 modified versions of the software inside them, although the manufacturer
52 can do so. This is fundamentally incompatible with the aim of
52 can do so. This is fundamentally incompatible with the aim of
53 protecting users' freedom to change the software. The systematic
53 protecting users' freedom to change the software. The systematic
54 pattern of such abuse occurs in the area of products for individuals to
54 pattern of such abuse occurs in the area of products for individuals to
55 use, which is precisely where it is most unacceptable. Therefore, we
55 use, which is precisely where it is most unacceptable. Therefore, we
56 have designed this version of the GPL to prohibit the practice for those
56 have designed this version of the GPL to prohibit the practice for those
57 products. If such problems arise substantially in other domains, we
57 products. If such problems arise substantially in other domains, we
58 stand ready to extend this provision to those domains in future versions
58 stand ready to extend this provision to those domains in future versions
59 of the GPL, as needed to protect the freedom of users.
59 of the GPL, as needed to protect the freedom of users.
60
60
61 Finally, every program is threatened constantly by software patents.
61 Finally, every program is threatened constantly by software patents.
62 States should not allow patents to restrict development and use of
62 States should not allow patents to restrict development and use of
63 software on general-purpose computers, but in those that do, we wish to
63 software on general-purpose computers, but in those that do, we wish to
64 avoid the special danger that patents applied to a free program could
64 avoid the special danger that patents applied to a free program could
65 make it effectively proprietary. To prevent this, the GPL assures that
65 make it effectively proprietary. To prevent this, the GPL assures that
66 patents cannot be used to render the program non-free.
66 patents cannot be used to render the program non-free.
67
67
68 The precise terms and conditions for copying, distribution and
68 The precise terms and conditions for copying, distribution and
69 modification follow.
69 modification follow.
70
70
71 TERMS AND CONDITIONS
71 TERMS AND CONDITIONS
72
72
73 0. Definitions.
73 0. Definitions.
74
74
75 "This License" refers to version 3 of the GNU General Public License.
75 "This License" refers to version 3 of the GNU General Public License.
76
76
77 "Copyright" also means copyright-like laws that apply to other kinds of
77 "Copyright" also means copyright-like laws that apply to other kinds of
78 works, such as semiconductor masks.
78 works, such as semiconductor masks.
79
79
80 "The Program" refers to any copyrightable work licensed under this
80 "The Program" refers to any copyrightable work licensed under this
81 License. Each licensee is addressed as "you". "Licensees" and
81 License. Each licensee is addressed as "you". "Licensees" and
82 "recipients" may be individuals or organizations.
82 "recipients" may be individuals or organizations.
83
83
84 To "modify" a work means to copy from or adapt all or part of the work
84 To "modify" a work means to copy from or adapt all or part of the work
85 in a fashion requiring copyright permission, other than the making of an
85 in a fashion requiring copyright permission, other than the making of an
86 exact copy. The resulting work is called a "modified version" of the
86 exact copy. The resulting work is called a "modified version" of the
87 earlier work or a work "based on" the earlier work.
87 earlier work or a work "based on" the earlier work.
88
88
89 A "covered work" means either the unmodified Program or a work based
89 A "covered work" means either the unmodified Program or a work based
90 on the Program.
90 on the Program.
91
91
92 To "propagate" a work means to do anything with it that, without
92 To "propagate" a work means to do anything with it that, without
93 permission, would make you directly or secondarily liable for
93 permission, would make you directly or secondarily liable for
94 infringement under applicable copyright law, except executing it on a
94 infringement under applicable copyright law, except executing it on a
95 computer or modifying a private copy. Propagation includes copying,
95 computer or modifying a private copy. Propagation includes copying,
96 distribution (with or without modification), making available to the
96 distribution (with or without modification), making available to the
97 public, and in some countries other activities as well.
97 public, and in some countries other activities as well.
98
98
99 To "convey" a work means any kind of propagation that enables other
99 To "convey" a work means any kind of propagation that enables other
100 parties to make or receive copies. Mere interaction with a user through
100 parties to make or receive copies. Mere interaction with a user through
101 a computer network, with no transfer of a copy, is not conveying.
101 a computer network, with no transfer of a copy, is not conveying.
102
102
103 An interactive user interface displays "Appropriate Legal Notices"
103 An interactive user interface displays "Appropriate Legal Notices"
104 to the extent that it includes a convenient and prominently visible
104 to the extent that it includes a convenient and prominently visible
105 feature that (1) displays an appropriate copyright notice, and (2)
105 feature that (1) displays an appropriate copyright notice, and (2)
106 tells the user that there is no warranty for the work (except to the
106 tells the user that there is no warranty for the work (except to the
107 extent that warranties are provided), that licensees may convey the
107 extent that warranties are provided), that licensees may convey the
108 work under this License, and how to view a copy of this License. If
108 work under this License, and how to view a copy of this License. If
109 the interface presents a list of user commands or options, such as a
109 the interface presents a list of user commands or options, such as a
110 menu, a prominent item in the list meets this criterion.
110 menu, a prominent item in the list meets this criterion.
111
111
112 1. Source Code.
112 1. Source Code.
113
113
114 The "source code" for a work means the preferred form of the work
114 The "source code" for a work means the preferred form of the work
115 for making modifications to it. "Object code" means any non-source
115 for making modifications to it. "Object code" means any non-source
116 form of a work.
116 form of a work.
117
117
118 A "Standard Interface" means an interface that either is an official
118 A "Standard Interface" means an interface that either is an official
119 standard defined by a recognized standards body, or, in the case of
119 standard defined by a recognized standards body, or, in the case of
120 interfaces specified for a particular programming language, one that
120 interfaces specified for a particular programming language, one that
121 is widely used among developers working in that language.
121 is widely used among developers working in that language.
122
122
123 The "System Libraries" of an executable work include anything, other
123 The "System Libraries" of an executable work include anything, other
124 than the work as a whole, that (a) is included in the normal form of
124 than the work as a whole, that (a) is included in the normal form of
125 packaging a Major Component, but which is not part of that Major
125 packaging a Major Component, but which is not part of that Major
126 Component, and (b) serves only to enable use of the work with that
126 Component, and (b) serves only to enable use of the work with that
127 Major Component, or to implement a Standard Interface for which an
127 Major Component, or to implement a Standard Interface for which an
128 implementation is available to the public in source code form. A
128 implementation is available to the public in source code form. A
129 "Major Component", in this context, means a major essential component
129 "Major Component", in this context, means a major essential component
130 (kernel, window system, and so on) of the specific operating system
130 (kernel, window system, and so on) of the specific operating system
131 (if any) on which the executable work runs, or a compiler used to
131 (if any) on which the executable work runs, or a compiler used to
132 produce the work, or an object code interpreter used to run it.
132 produce the work, or an object code interpreter used to run it.
133
133
134 The "Corresponding Source" for a work in object code form means all
134 The "Corresponding Source" for a work in object code form means all
135 the source code needed to generate, install, and (for an executable
135 the source code needed to generate, install, and (for an executable
136 work) run the object code and to modify the work, including scripts to
136 work) run the object code and to modify the work, including scripts to
137 control those activities. However, it does not include the work's
137 control those activities. However, it does not include the work's
138 System Libraries, or general-purpose tools or generally available free
138 System Libraries, or general-purpose tools or generally available free
139 programs which are used unmodified in performing those activities but
139 programs which are used unmodified in performing those activities but
140 which are not part of the work. For example, Corresponding Source
140 which are not part of the work. For example, Corresponding Source
141 includes interface definition files associated with source files for
141 includes interface definition files associated with source files for
142 the work, and the source code for shared libraries and dynamically
142 the work, and the source code for shared libraries and dynamically
143 linked subprograms that the work is specifically designed to require,
143 linked subprograms that the work is specifically designed to require,
144 such as by intimate data communication or control flow between those
144 such as by intimate data communication or control flow between those
145 subprograms and other parts of the work.
145 subprograms and other parts of the work.
146
146
147 The Corresponding Source need not include anything that users
147 The Corresponding Source need not include anything that users
148 can regenerate automatically from other parts of the Corresponding
148 can regenerate automatically from other parts of the Corresponding
149 Source.
149 Source.
150
150
151 The Corresponding Source for a work in source code form is that
151 The Corresponding Source for a work in source code form is that
152 same work.
152 same work.
153
153
154 2. Basic Permissions.
154 2. Basic Permissions.
155
155
156 All rights granted under this License are granted for the term of
156 All rights granted under this License are granted for the term of
157 copyright on the Program, and are irrevocable provided the stated
157 copyright on the Program, and are irrevocable provided the stated
158 conditions are met. This License explicitly affirms your unlimited
158 conditions are met. This License explicitly affirms your unlimited
159 permission to run the unmodified Program. The output from running a
159 permission to run the unmodified Program. The output from running a
160 covered work is covered by this License only if the output, given its
160 covered work is covered by this License only if the output, given its
161 content, constitutes a covered work. This License acknowledges your
161 content, constitutes a covered work. This License acknowledges your
162 rights of fair use or other equivalent, as provided by copyright law.
162 rights of fair use or other equivalent, as provided by copyright law.
163
163
164 You may make, run and propagate covered works that you do not
164 You may make, run and propagate covered works that you do not
165 convey, without conditions so long as your license otherwise remains
165 convey, without conditions so long as your license otherwise remains
166 in force. You may convey covered works to others for the sole purpose
166 in force. You may convey covered works to others for the sole purpose
167 of having them make modifications exclusively for you, or provide you
167 of having them make modifications exclusively for you, or provide you
168 with facilities for running those works, provided that you comply with
168 with facilities for running those works, provided that you comply with
169 the terms of this License in conveying all material for which you do
169 the terms of this License in conveying all material for which you do
170 not control copyright. Those thus making or running the covered works
170 not control copyright. Those thus making or running the covered works
171 for you must do so exclusively on your behalf, under your direction
171 for you must do so exclusively on your behalf, under your direction
172 and control, on terms that prohibit them from making any copies of
172 and control, on terms that prohibit them from making any copies of
173 your copyrighted material outside their relationship with you.
173 your copyrighted material outside their relationship with you.
174
174
175 Conveying under any other circumstances is permitted solely under
175 Conveying under any other circumstances is permitted solely under
176 the conditions stated below. Sublicensing is not allowed; section 10
176 the conditions stated below. Sublicensing is not allowed; section 10
177 makes it unnecessary.
177 makes it unnecessary.
178
178
179 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
179 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180
180
181 No covered work shall be deemed part of an effective technological
181 No covered work shall be deemed part of an effective technological
182 measure under any applicable law fulfilling obligations under article
182 measure under any applicable law fulfilling obligations under article
183 11 of the WIPO copyright treaty adopted on 20 December 1996, or
183 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 similar laws prohibiting or restricting circumvention of such
184 similar laws prohibiting or restricting circumvention of such
185 measures.
185 measures.
186
186
187 When you convey a covered work, you waive any legal power to forbid
187 When you convey a covered work, you waive any legal power to forbid
188 circumvention of technological measures to the extent such circumvention
188 circumvention of technological measures to the extent such circumvention
189 is effected by exercising rights under this License with respect to
189 is effected by exercising rights under this License with respect to
190 the covered work, and you disclaim any intention to limit operation or
190 the covered work, and you disclaim any intention to limit operation or
191 modification of the work as a means of enforcing, against the work's
191 modification of the work as a means of enforcing, against the work's
192 users, your or third parties' legal rights to forbid circumvention of
192 users, your or third parties' legal rights to forbid circumvention of
193 technological measures.
193 technological measures.
194
194
195 4. Conveying Verbatim Copies.
195 4. Conveying Verbatim Copies.
196
196
197 You may convey verbatim copies of the Program's source code as you
197 You may convey verbatim copies of the Program's source code as you
198 receive it, in any medium, provided that you conspicuously and
198 receive it, in any medium, provided that you conspicuously and
199 appropriately publish on each copy an appropriate copyright notice;
199 appropriately publish on each copy an appropriate copyright notice;
200 keep intact all notices stating that this License and any
200 keep intact all notices stating that this License and any
201 non-permissive terms added in accord with section 7 apply to the code;
201 non-permissive terms added in accord with section 7 apply to the code;
202 keep intact all notices of the absence of any warranty; and give all
202 keep intact all notices of the absence of any warranty; and give all
203 recipients a copy of this License along with the Program.
203 recipients a copy of this License along with the Program.
204
204
205 You may charge any price or no price for each copy that you convey,
205 You may charge any price or no price for each copy that you convey,
206 and you may offer support or warranty protection for a fee.
206 and you may offer support or warranty protection for a fee.
207
207
208 5. Conveying Modified Source Versions.
208 5. Conveying Modified Source Versions.
209
209
210 You may convey a work based on the Program, or the modifications to
210 You may convey a work based on the Program, or the modifications to
211 produce it from the Program, in the form of source code under the
211 produce it from the Program, in the form of source code under the
212 terms of section 4, provided that you also meet all of these conditions:
212 terms of section 4, provided that you also meet all of these conditions:
213
213
214 a) The work must carry prominent notices stating that you modified
214 a) The work must carry prominent notices stating that you modified
215 it, and giving a relevant date.
215 it, and giving a relevant date.
216
216
217 b) The work must carry prominent notices stating that it is
217 b) The work must carry prominent notices stating that it is
218 released under this License and any conditions added under section
218 released under this License and any conditions added under section
219 7. This requirement modifies the requirement in section 4 to
219 7. This requirement modifies the requirement in section 4 to
220 "keep intact all notices".
220 "keep intact all notices".
221
221
222 c) You must license the entire work, as a whole, under this
222 c) You must license the entire work, as a whole, under this
223 License to anyone who comes into possession of a copy. This
223 License to anyone who comes into possession of a copy. This
224 License will therefore apply, along with any applicable section 7
224 License will therefore apply, along with any applicable section 7
225 additional terms, to the whole of the work, and all its parts,
225 additional terms, to the whole of the work, and all its parts,
226 regardless of how they are packaged. This License gives no
226 regardless of how they are packaged. This License gives no
227 permission to license the work in any other way, but it does not
227 permission to license the work in any other way, but it does not
228 invalidate such permission if you have separately received it.
228 invalidate such permission if you have separately received it.
229
229
230 d) If the work has interactive user interfaces, each must display
230 d) If the work has interactive user interfaces, each must display
231 Appropriate Legal Notices; however, if the Program has interactive
231 Appropriate Legal Notices; however, if the Program has interactive
232 interfaces that do not display Appropriate Legal Notices, your
232 interfaces that do not display Appropriate Legal Notices, your
233 work need not make them do so.
233 work need not make them do so.
234
234
235 A compilation of a covered work with other separate and independent
235 A compilation of a covered work with other separate and independent
236 works, which are not by their nature extensions of the covered work,
236 works, which are not by their nature extensions of the covered work,
237 and which are not combined with it such as to form a larger program,
237 and which are not combined with it such as to form a larger program,
238 in or on a volume of a storage or distribution medium, is called an
238 in or on a volume of a storage or distribution medium, is called an
239 "aggregate" if the compilation and its resulting copyright are not
239 "aggregate" if the compilation and its resulting copyright are not
240 used to limit the access or legal rights of the compilation's users
240 used to limit the access or legal rights of the compilation's users
241 beyond what the individual works permit. Inclusion of a covered work
241 beyond what the individual works permit. Inclusion of a covered work
242 in an aggregate does not cause this License to apply to the other
242 in an aggregate does not cause this License to apply to the other
243 parts of the aggregate.
243 parts of the aggregate.
244
244
245 6. Conveying Non-Source Forms.
245 6. Conveying Non-Source Forms.
246
246
247 You may convey a covered work in object code form under the terms
247 You may convey a covered work in object code form under the terms
248 of sections 4 and 5, provided that you also convey the
248 of sections 4 and 5, provided that you also convey the
249 machine-readable Corresponding Source under the terms of this License,
249 machine-readable Corresponding Source under the terms of this License,
250 in one of these ways:
250 in one of these ways:
251
251
252 a) Convey the object code in, or embodied in, a physical product
252 a) Convey the object code in, or embodied in, a physical product
253 (including a physical distribution medium), accompanied by the
253 (including a physical distribution medium), accompanied by the
254 Corresponding Source fixed on a durable physical medium
254 Corresponding Source fixed on a durable physical medium
255 customarily used for software interchange.
255 customarily used for software interchange.
256
256
257 b) Convey the object code in, or embodied in, a physical product
257 b) Convey the object code in, or embodied in, a physical product
258 (including a physical distribution medium), accompanied by a
258 (including a physical distribution medium), accompanied by a
259 written offer, valid for at least three years and valid for as
259 written offer, valid for at least three years and valid for as
260 long as you offer spare parts or customer support for that product
260 long as you offer spare parts or customer support for that product
261 model, to give anyone who possesses the object code either (1) a
261 model, to give anyone who possesses the object code either (1) a
262 copy of the Corresponding Source for all the software in the
262 copy of the Corresponding Source for all the software in the
263 product that is covered by this License, on a durable physical
263 product that is covered by this License, on a durable physical
264 medium customarily used for software interchange, for a price no
264 medium customarily used for software interchange, for a price no
265 more than your reasonable cost of physically performing this
265 more than your reasonable cost of physically performing this
266 conveying of source, or (2) access to copy the
266 conveying of source, or (2) access to copy the
267 Corresponding Source from a network server at no charge.
267 Corresponding Source from a network server at no charge.
268
268
269 c) Convey individual copies of the object code with a copy of the
269 c) Convey individual copies of the object code with a copy of the
270 written offer to provide the Corresponding Source. This
270 written offer to provide the Corresponding Source. This
271 alternative is allowed only occasionally and noncommercially, and
271 alternative is allowed only occasionally and noncommercially, and
272 only if you received the object code with such an offer, in accord
272 only if you received the object code with such an offer, in accord
273 with subsection 6b.
273 with subsection 6b.
274
274
275 d) Convey the object code by offering access from a designated
275 d) Convey the object code by offering access from a designated
276 place (gratis or for a charge), and offer equivalent access to the
276 place (gratis or for a charge), and offer equivalent access to the
277 Corresponding Source in the same way through the same place at no
277 Corresponding Source in the same way through the same place at no
278 further charge. You need not require recipients to copy the
278 further charge. You need not require recipients to copy the
279 Corresponding Source along with the object code. If the place to
279 Corresponding Source along with the object code. If the place to
280 copy the object code is a network server, the Corresponding Source
280 copy the object code is a network server, the Corresponding Source
281 may be on a different server (operated by you or a third party)
281 may be on a different server (operated by you or a third party)
282 that supports equivalent copying facilities, provided you maintain
282 that supports equivalent copying facilities, provided you maintain
283 clear directions next to the object code saying where to find the
283 clear directions next to the object code saying where to find the
284 Corresponding Source. Regardless of what server hosts the
284 Corresponding Source. Regardless of what server hosts the
285 Corresponding Source, you remain obligated to ensure that it is
285 Corresponding Source, you remain obligated to ensure that it is
286 available for as long as needed to satisfy these requirements.
286 available for as long as needed to satisfy these requirements.
287
287
288 e) Convey the object code using peer-to-peer transmission, provided
288 e) Convey the object code using peer-to-peer transmission, provided
289 you inform other peers where the object code and Corresponding
289 you inform other peers where the object code and Corresponding
290 Source of the work are being offered to the general public at no
290 Source of the work are being offered to the general public at no
291 charge under subsection 6d.
291 charge under subsection 6d.
292
292
293 A separable portion of the object code, whose source code is excluded
293 A separable portion of the object code, whose source code is excluded
294 from the Corresponding Source as a System Library, need not be
294 from the Corresponding Source as a System Library, need not be
295 included in conveying the object code work.
295 included in conveying the object code work.
296
296
297 A "User Product" is either (1) a "consumer product", which means any
297 A "User Product" is either (1) a "consumer product", which means any
298 tangible personal property which is normally used for personal, family,
298 tangible personal property which is normally used for personal, family,
299 or household purposes, or (2) anything designed or sold for incorporation
299 or household purposes, or (2) anything designed or sold for incorporation
300 into a dwelling. In determining whether a product is a consumer product,
300 into a dwelling. In determining whether a product is a consumer product,
301 doubtful cases shall be resolved in favor of coverage. For a particular
301 doubtful cases shall be resolved in favor of coverage. For a particular
302 product received by a particular user, "normally used" refers to a
302 product received by a particular user, "normally used" refers to a
303 typical or common use of that class of product, regardless of the status
303 typical or common use of that class of product, regardless of the status
304 of the particular user or of the way in which the particular user
304 of the particular user or of the way in which the particular user
305 actually uses, or expects or is expected to use, the product. A product
305 actually uses, or expects or is expected to use, the product. A product
306 is a consumer product regardless of whether the product has substantial
306 is a consumer product regardless of whether the product has substantial
307 commercial, industrial or non-consumer uses, unless such uses represent
307 commercial, industrial or non-consumer uses, unless such uses represent
308 the only significant mode of use of the product.
308 the only significant mode of use of the product.
309
309
310 "Installation Information" for a User Product means any methods,
310 "Installation Information" for a User Product means any methods,
311 procedures, authorization keys, or other information required to install
311 procedures, authorization keys, or other information required to install
312 and execute modified versions of a covered work in that User Product from
312 and execute modified versions of a covered work in that User Product from
313 a modified version of its Corresponding Source. The information must
313 a modified version of its Corresponding Source. The information must
314 suffice to ensure that the continued functioning of the modified object
314 suffice to ensure that the continued functioning of the modified object
315 code is in no case prevented or interfered with solely because
315 code is in no case prevented or interfered with solely because
316 modification has been made.
316 modification has been made.
317
317
318 If you convey an object code work under this section in, or with, or
318 If you convey an object code work under this section in, or with, or
319 specifically for use in, a User Product, and the conveying occurs as
319 specifically for use in, a User Product, and the conveying occurs as
320 part of a transaction in which the right of possession and use of the
320 part of a transaction in which the right of possession and use of the
321 User Product is transferred to the recipient in perpetuity or for a
321 User Product is transferred to the recipient in perpetuity or for a
322 fixed term (regardless of how the transaction is characterized), the
322 fixed term (regardless of how the transaction is characterized), the
323 Corresponding Source conveyed under this section must be accompanied
323 Corresponding Source conveyed under this section must be accompanied
324 by the Installation Information. But this requirement does not apply
324 by the Installation Information. But this requirement does not apply
325 if neither you nor any third party retains the ability to install
325 if neither you nor any third party retains the ability to install
326 modified object code on the User Product (for example, the work has
326 modified object code on the User Product (for example, the work has
327 been installed in ROM).
327 been installed in ROM).
328
328
329 The requirement to provide Installation Information does not include a
329 The requirement to provide Installation Information does not include a
330 requirement to continue to provide support service, warranty, or updates
330 requirement to continue to provide support service, warranty, or updates
331 for a work that has been modified or installed by the recipient, or for
331 for a work that has been modified or installed by the recipient, or for
332 the User Product in which it has been modified or installed. Access to a
332 the User Product in which it has been modified or installed. Access to a
333 network may be denied when the modification itself materially and
333 network may be denied when the modification itself materially and
334 adversely affects the operation of the network or violates the rules and
334 adversely affects the operation of the network or violates the rules and
335 protocols for communication across the network.
335 protocols for communication across the network.
336
336
337 Corresponding Source conveyed, and Installation Information provided,
337 Corresponding Source conveyed, and Installation Information provided,
338 in accord with this section must be in a format that is publicly
338 in accord with this section must be in a format that is publicly
339 documented (and with an implementation available to the public in
339 documented (and with an implementation available to the public in
340 source code form), and must require no special password or key for
340 source code form), and must require no special password or key for
341 unpacking, reading or copying.
341 unpacking, reading or copying.
342
342
343 7. Additional Terms.
343 7. Additional Terms.
344
344
345 "Additional permissions" are terms that supplement the terms of this
345 "Additional permissions" are terms that supplement the terms of this
346 License by making exceptions from one or more of its conditions.
346 License by making exceptions from one or more of its conditions.
347 Additional permissions that are applicable to the entire Program shall
347 Additional permissions that are applicable to the entire Program shall
348 be treated as though they were included in this License, to the extent
348 be treated as though they were included in this License, to the extent
349 that they are valid under applicable law. If additional permissions
349 that they are valid under applicable law. If additional permissions
350 apply only to part of the Program, that part may be used separately
350 apply only to part of the Program, that part may be used separately
351 under those permissions, but the entire Program remains governed by
351 under those permissions, but the entire Program remains governed by
352 this License without regard to the additional permissions.
352 this License without regard to the additional permissions.
353
353
354 When you convey a copy of a covered work, you may at your option
354 When you convey a copy of a covered work, you may at your option
355 remove any additional permissions from that copy, or from any part of
355 remove any additional permissions from that copy, or from any part of
356 it. (Additional permissions may be written to require their own
356 it. (Additional permissions may be written to require their own
357 removal in certain cases when you modify the work.) You may place
357 removal in certain cases when you modify the work.) You may place
358 additional permissions on material, added by you to a covered work,
358 additional permissions on material, added by you to a covered work,
359 for which you have or can give appropriate copyright permission.
359 for which you have or can give appropriate copyright permission.
360
360
361 Notwithstanding any other provision of this License, for material you
361 Notwithstanding any other provision of this License, for material you
362 add to a covered work, you may (if authorized by the copyright holders of
362 add to a covered work, you may (if authorized by the copyright holders of
363 that material) supplement the terms of this License with terms:
363 that material) supplement the terms of this License with terms:
364
364
365 a) Disclaiming warranty or limiting liability differently from the
365 a) Disclaiming warranty or limiting liability differently from the
366 terms of sections 15 and 16 of this License; or
366 terms of sections 15 and 16 of this License; or
367
367
368 b) Requiring preservation of specified reasonable legal notices or
368 b) Requiring preservation of specified reasonable legal notices or
369 author attributions in that material or in the Appropriate Legal
369 author attributions in that material or in the Appropriate Legal
370 Notices displayed by works containing it; or
370 Notices displayed by works containing it; or
371
371
372 c) Prohibiting misrepresentation of the origin of that material, or
372 c) Prohibiting misrepresentation of the origin of that material, or
373 requiring that modified versions of such material be marked in
373 requiring that modified versions of such material be marked in
374 reasonable ways as different from the original version; or
374 reasonable ways as different from the original version; or
375
375
376 d) Limiting the use for publicity purposes of names of licensors or
376 d) Limiting the use for publicity purposes of names of licensors or
377 authors of the material; or
377 authors of the material; or
378
378
379 e) Declining to grant rights under trademark law for use of some
379 e) Declining to grant rights under trademark law for use of some
380 trade names, trademarks, or service marks; or
380 trade names, trademarks, or service marks; or
381
381
382 f) Requiring indemnification of licensors and authors of that
382 f) Requiring indemnification of licensors and authors of that
383 material by anyone who conveys the material (or modified versions of
383 material by anyone who conveys the material (or modified versions of
384 it) with contractual assumptions of liability to the recipient, for
384 it) with contractual assumptions of liability to the recipient, for
385 any liability that these contractual assumptions directly impose on
385 any liability that these contractual assumptions directly impose on
386 those licensors and authors.
386 those licensors and authors.
387
387
388 All other non-permissive additional terms are considered "further
388 All other non-permissive additional terms are considered "further
389 restrictions" within the meaning of section 10. If the Program as you
389 restrictions" within the meaning of section 10. If the Program as you
390 received it, or any part of it, contains a notice stating that it is
390 received it, or any part of it, contains a notice stating that it is
391 governed by this License along with a term that is a further
391 governed by this License along with a term that is a further
392 restriction, you may remove that term. If a license document contains
392 restriction, you may remove that term. If a license document contains
393 a further restriction but permits relicensing or conveying under this
393 a further restriction but permits relicensing or conveying under this
394 License, you may add to a covered work material governed by the terms
394 License, you may add to a covered work material governed by the terms
395 of that license document, provided that the further restriction does
395 of that license document, provided that the further restriction does
396 not survive such relicensing or conveying.
396 not survive such relicensing or conveying.
397
397
398 If you add terms to a covered work in accord with this section, you
398 If you add terms to a covered work in accord with this section, you
399 must place, in the relevant source files, a statement of the
399 must place, in the relevant source files, a statement of the
400 additional terms that apply to those files, or a notice indicating
400 additional terms that apply to those files, or a notice indicating
401 where to find the applicable terms.
401 where to find the applicable terms.
402
402
403 Additional terms, permissive or non-permissive, may be stated in the
403 Additional terms, permissive or non-permissive, may be stated in the
404 form of a separately written license, or stated as exceptions;
404 form of a separately written license, or stated as exceptions;
405 the above requirements apply either way.
405 the above requirements apply either way.
406
406
407 8. Termination.
407 8. Termination.
408
408
409 You may not propagate or modify a covered work except as expressly
409 You may not propagate or modify a covered work except as expressly
410 provided under this License. Any attempt otherwise to propagate or
410 provided under this License. Any attempt otherwise to propagate or
411 modify it is void, and will automatically terminate your rights under
411 modify it is void, and will automatically terminate your rights under
412 this License (including any patent licenses granted under the third
412 this License (including any patent licenses granted under the third
413 paragraph of section 11).
413 paragraph of section 11).
414
414
415 However, if you cease all violation of this License, then your
415 However, if you cease all violation of this License, then your
416 license from a particular copyright holder is reinstated (a)
416 license from a particular copyright holder is reinstated (a)
417 provisionally, unless and until the copyright holder explicitly and
417 provisionally, unless and until the copyright holder explicitly and
418 finally terminates your license, and (b) permanently, if the copyright
418 finally terminates your license, and (b) permanently, if the copyright
419 holder fails to notify you of the violation by some reasonable means
419 holder fails to notify you of the violation by some reasonable means
420 prior to 60 days after the cessation.
420 prior to 60 days after the cessation.
421
421
422 Moreover, your license from a particular copyright holder is
422 Moreover, your license from a particular copyright holder is
423 reinstated permanently if the copyright holder notifies you of the
423 reinstated permanently if the copyright holder notifies you of the
424 violation by some reasonable means, this is the first time you have
424 violation by some reasonable means, this is the first time you have
425 received notice of violation of this License (for any work) from that
425 received notice of violation of this License (for any work) from that
426 copyright holder, and you cure the violation prior to 30 days after
426 copyright holder, and you cure the violation prior to 30 days after
427 your receipt of the notice.
427 your receipt of the notice.
428
428
429 Termination of your rights under this section does not terminate the
429 Termination of your rights under this section does not terminate the
430 licenses of parties who have received copies or rights from you under
430 licenses of parties who have received copies or rights from you under
431 this License. If your rights have been terminated and not permanently
431 this License. If your rights have been terminated and not permanently
432 reinstated, you do not qualify to receive new licenses for the same
432 reinstated, you do not qualify to receive new licenses for the same
433 material under section 10.
433 material under section 10.
434
434
435 9. Acceptance Not Required for Having Copies.
435 9. Acceptance Not Required for Having Copies.
436
436
437 You are not required to accept this License in order to receive or
437 You are not required to accept this License in order to receive or
438 run a copy of the Program. Ancillary propagation of a covered work
438 run a copy of the Program. Ancillary propagation of a covered work
439 occurring solely as a consequence of using peer-to-peer transmission
439 occurring solely as a consequence of using peer-to-peer transmission
440 to receive a copy likewise does not require acceptance. However,
440 to receive a copy likewise does not require acceptance. However,
441 nothing other than this License grants you permission to propagate or
441 nothing other than this License grants you permission to propagate or
442 modify any covered work. These actions infringe copyright if you do
442 modify any covered work. These actions infringe copyright if you do
443 not accept this License. Therefore, by modifying or propagating a
443 not accept this License. Therefore, by modifying or propagating a
444 covered work, you indicate your acceptance of this License to do so.
444 covered work, you indicate your acceptance of this License to do so.
445
445
446 10. Automatic Licensing of Downstream Recipients.
446 10. Automatic Licensing of Downstream Recipients.
447
447
448 Each time you convey a covered work, the recipient automatically
448 Each time you convey a covered work, the recipient automatically
449 receives a license from the original licensors, to run, modify and
449 receives a license from the original licensors, to run, modify and
450 propagate that work, subject to this License. You are not responsible
450 propagate that work, subject to this License. You are not responsible
451 for enforcing compliance by third parties with this License.
451 for enforcing compliance by third parties with this License.
452
452
453 An "entity transaction" is a transaction transferring control of an
453 An "entity transaction" is a transaction transferring control of an
454 organization, or substantially all assets of one, or subdividing an
454 organization, or substantially all assets of one, or subdividing an
455 organization, or merging organizations. If propagation of a covered
455 organization, or merging organizations. If propagation of a covered
456 work results from an entity transaction, each party to that
456 work results from an entity transaction, each party to that
457 transaction who receives a copy of the work also receives whatever
457 transaction who receives a copy of the work also receives whatever
458 licenses to the work the party's predecessor in interest had or could
458 licenses to the work the party's predecessor in interest had or could
459 give under the previous paragraph, plus a right to possession of the
459 give under the previous paragraph, plus a right to possession of the
460 Corresponding Source of the work from the predecessor in interest, if
460 Corresponding Source of the work from the predecessor in interest, if
461 the predecessor has it or can get it with reasonable efforts.
461 the predecessor has it or can get it with reasonable efforts.
462
462
463 You may not impose any further restrictions on the exercise of the
463 You may not impose any further restrictions on the exercise of the
464 rights granted or affirmed under this License. For example, you may
464 rights granted or affirmed under this License. For example, you may
465 not impose a license fee, royalty, or other charge for exercise of
465 not impose a license fee, royalty, or other charge for exercise of
466 rights granted under this License, and you may not initiate litigation
466 rights granted under this License, and you may not initiate litigation
467 (including a cross-claim or counterclaim in a lawsuit) alleging that
467 (including a cross-claim or counterclaim in a lawsuit) alleging that
468 any patent claim is infringed by making, using, selling, offering for
468 any patent claim is infringed by making, using, selling, offering for
469 sale, or importing the Program or any portion of it.
469 sale, or importing the Program or any portion of it.
470
470
471 11. Patents.
471 11. Patents.
472
472
473 A "contributor" is a copyright holder who authorizes use under this
473 A "contributor" is a copyright holder who authorizes use under this
474 License of the Program or a work on which the Program is based. The
474 License of the Program or a work on which the Program is based. The
475 work thus licensed is called the contributor's "contributor version".
475 work thus licensed is called the contributor's "contributor version".
476
476
477 A contributor's "essential patent claims" are all patent claims
477 A contributor's "essential patent claims" are all patent claims
478 owned or controlled by the contributor, whether already acquired or
478 owned or controlled by the contributor, whether already acquired or
479 hereafter acquired, that would be infringed by some manner, permitted
479 hereafter acquired, that would be infringed by some manner, permitted
480 by this License, of making, using, or selling its contributor version,
480 by this License, of making, using, or selling its contributor version,
481 but do not include claims that would be infringed only as a
481 but do not include claims that would be infringed only as a
482 consequence of further modification of the contributor version. For
482 consequence of further modification of the contributor version. For
483 purposes of this definition, "control" includes the right to grant
483 purposes of this definition, "control" includes the right to grant
484 patent sublicenses in a manner consistent with the requirements of
484 patent sublicenses in a manner consistent with the requirements of
485 this License.
485 this License.
486
486
487 Each contributor grants you a non-exclusive, worldwide, royalty-free
487 Each contributor grants you a non-exclusive, worldwide, royalty-free
488 patent license under the contributor's essential patent claims, to
488 patent license under the contributor's essential patent claims, to
489 make, use, sell, offer for sale, import and otherwise run, modify and
489 make, use, sell, offer for sale, import and otherwise run, modify and
490 propagate the contents of its contributor version.
490 propagate the contents of its contributor version.
491
491
492 In the following three paragraphs, a "patent license" is any express
492 In the following three paragraphs, a "patent license" is any express
493 agreement or commitment, however denominated, not to enforce a patent
493 agreement or commitment, however denominated, not to enforce a patent
494 (such as an express permission to practice a patent or covenant not to
494 (such as an express permission to practice a patent or covenant not to
495 sue for patent infringement). To "grant" such a patent license to a
495 sue for patent infringement). To "grant" such a patent license to a
496 party means to make such an agreement or commitment not to enforce a
496 party means to make such an agreement or commitment not to enforce a
497 patent against the party.
497 patent against the party.
498
498
499 If you convey a covered work, knowingly relying on a patent license,
499 If you convey a covered work, knowingly relying on a patent license,
500 and the Corresponding Source of the work is not available for anyone
500 and the Corresponding Source of the work is not available for anyone
501 to copy, free of charge and under the terms of this License, through a
501 to copy, free of charge and under the terms of this License, through a
502 publicly available network server or other readily accessible means,
502 publicly available network server or other readily accessible means,
503 then you must either (1) cause the Corresponding Source to be so
503 then you must either (1) cause the Corresponding Source to be so
504 available, or (2) arrange to deprive yourself of the benefit of the
504 available, or (2) arrange to deprive yourself of the benefit of the
505 patent license for this particular work, or (3) arrange, in a manner
505 patent license for this particular work, or (3) arrange, in a manner
506 consistent with the requirements of this License, to extend the patent
506 consistent with the requirements of this License, to extend the patent
507 license to downstream recipients. "Knowingly relying" means you have
507 license to downstream recipients. "Knowingly relying" means you have
508 actual knowledge that, but for the patent license, your conveying the
508 actual knowledge that, but for the patent license, your conveying the
509 covered work in a country, or your recipient's use of the covered work
509 covered work in a country, or your recipient's use of the covered work
510 in a country, would infringe one or more identifiable patents in that
510 in a country, would infringe one or more identifiable patents in that
511 country that you have reason to believe are valid.
511 country that you have reason to believe are valid.
512
512
513 If, pursuant to or in connection with a single transaction or
513 If, pursuant to or in connection with a single transaction or
514 arrangement, you convey, or propagate by procuring conveyance of, a
514 arrangement, you convey, or propagate by procuring conveyance of, a
515 covered work, and grant a patent license to some of the parties
515 covered work, and grant a patent license to some of the parties
516 receiving the covered work authorizing them to use, propagate, modify
516 receiving the covered work authorizing them to use, propagate, modify
517 or convey a specific copy of the covered work, then the patent license
517 or convey a specific copy of the covered work, then the patent license
518 you grant is automatically extended to all recipients of the covered
518 you grant is automatically extended to all recipients of the covered
519 work and works based on it.
519 work and works based on it.
520
520
521 A patent license is "discriminatory" if it does not include within
521 A patent license is "discriminatory" if it does not include within
522 the scope of its coverage, prohibits the exercise of, or is
522 the scope of its coverage, prohibits the exercise of, or is
523 conditioned on the non-exercise of one or more of the rights that are
523 conditioned on the non-exercise of one or more of the rights that are
524 specifically granted under this License. You may not convey a covered
524 specifically granted under this License. You may not convey a covered
525 work if you are a party to an arrangement with a third party that is
525 work if you are a party to an arrangement with a third party that is
526 in the business of distributing software, under which you make payment
526 in the business of distributing software, under which you make payment
527 to the third party based on the extent of your activity of conveying
527 to the third party based on the extent of your activity of conveying
528 the work, and under which the third party grants, to any of the
528 the work, and under which the third party grants, to any of the
529 parties who would receive the covered work from you, a discriminatory
529 parties who would receive the covered work from you, a discriminatory
530 patent license (a) in connection with copies of the covered work
530 patent license (a) in connection with copies of the covered work
531 conveyed by you (or copies made from those copies), or (b) primarily
531 conveyed by you (or copies made from those copies), or (b) primarily
532 for and in connection with specific products or compilations that
532 for and in connection with specific products or compilations that
533 contain the covered work, unless you entered into that arrangement,
533 contain the covered work, unless you entered into that arrangement,
534 or that patent license was granted, prior to 28 March 2007.
534 or that patent license was granted, prior to 28 March 2007.
535
535
536 Nothing in this License shall be construed as excluding or limiting
536 Nothing in this License shall be construed as excluding or limiting
537 any implied license or other defenses to infringement that may
537 any implied license or other defenses to infringement that may
538 otherwise be available to you under applicable patent law.
538 otherwise be available to you under applicable patent law.
539
539
540 12. No Surrender of Others' Freedom.
540 12. No Surrender of Others' Freedom.
541
541
542 If conditions are imposed on you (whether by court order, agreement or
542 If conditions are imposed on you (whether by court order, agreement or
543 otherwise) that contradict the conditions of this License, they do not
543 otherwise) that contradict the conditions of this License, they do not
544 excuse you from the conditions of this License. If you cannot convey a
544 excuse you from the conditions of this License. If you cannot convey a
545 covered work so as to satisfy simultaneously your obligations under this
545 covered work so as to satisfy simultaneously your obligations under this
546 License and any other pertinent obligations, then as a consequence you may
546 License and any other pertinent obligations, then as a consequence you may
547 not convey it at all. For example, if you agree to terms that obligate you
547 not convey it at all. For example, if you agree to terms that obligate you
548 to collect a royalty for further conveying from those to whom you convey
548 to collect a royalty for further conveying from those to whom you convey
549 the Program, the only way you could satisfy both those terms and this
549 the Program, the only way you could satisfy both those terms and this
550 License would be to refrain entirely from conveying the Program.
550 License would be to refrain entirely from conveying the Program.
551
551
552 13. Use with the GNU Affero General Public License.
552 13. Use with the GNU Affero General Public License.
553
553
554 Notwithstanding any other provision of this License, you have
554 Notwithstanding any other provision of this License, you have
555 permission to link or combine any covered work with a work licensed
555 permission to link or combine any covered work with a work licensed
556 under version 3 of the GNU Affero General Public License into a single
556 under version 3 of the GNU Affero General Public License into a single
557 combined work, and to convey the resulting work. The terms of this
557 combined work, and to convey the resulting work. The terms of this
558 License will continue to apply to the part which is the covered work,
558 License will continue to apply to the part which is the covered work,
559 but the special requirements of the GNU Affero General Public License,
559 but the special requirements of the GNU Affero General Public License,
560 section 13, concerning interaction through a network will apply to the
560 section 13, concerning interaction through a network will apply to the
561 combination as such.
561 combination as such.
562
562
563 14. Revised Versions of this License.
563 14. Revised Versions of this License.
564
564
565 The Free Software Foundation may publish revised and/or new versions of
565 The Free Software Foundation may publish revised and/or new versions of
566 the GNU General Public License from time to time. Such new versions will
566 the GNU General Public License from time to time. Such new versions will
567 be similar in spirit to the present version, but may differ in detail to
567 be similar in spirit to the present version, but may differ in detail to
568 address new problems or concerns.
568 address new problems or concerns.
569
569
570 Each version is given a distinguishing version number. If the
570 Each version is given a distinguishing version number. If the
571 Program specifies that a certain numbered version of the GNU General
571 Program specifies that a certain numbered version of the GNU General
572 Public License "or any later version" applies to it, you have the
572 Public License "or any later version" applies to it, you have the
573 option of following the terms and conditions either of that numbered
573 option of following the terms and conditions either of that numbered
574 version or of any later version published by the Free Software
574 version or of any later version published by the Free Software
575 Foundation. If the Program does not specify a version number of the
575 Foundation. If the Program does not specify a version number of the
576 GNU General Public License, you may choose any version ever published
576 GNU General Public License, you may choose any version ever published
577 by the Free Software Foundation.
577 by the Free Software Foundation.
578
578
579 If the Program specifies that a proxy can decide which future
579 If the Program specifies that a proxy can decide which future
580 versions of the GNU General Public License can be used, that proxy's
580 versions of the GNU General Public License can be used, that proxy's
581 public statement of acceptance of a version permanently authorizes you
581 public statement of acceptance of a version permanently authorizes you
582 to choose that version for the Program.
582 to choose that version for the Program.
583
583
584 Later license versions may give you additional or different
584 Later license versions may give you additional or different
585 permissions. However, no additional obligations are imposed on any
585 permissions. However, no additional obligations are imposed on any
586 author or copyright holder as a result of your choosing to follow a
586 author or copyright holder as a result of your choosing to follow a
587 later version.
587 later version.
588
588
589 15. Disclaimer of Warranty.
589 15. Disclaimer of Warranty.
590
590
591 THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
591 THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
592 APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
593 HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
594 OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
595 THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
596 PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
597 IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
598 ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599
599
600 16. Limitation of Liability.
600 16. Limitation of Liability.
601
601
602 IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
602 IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
603 WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
604 THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
605 GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
606 USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
607 DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
608 PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
609 EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 SUCH DAMAGES.
610 SUCH DAMAGES.
611
611
612 17. Interpretation of Sections 15 and 16.
612 17. Interpretation of Sections 15 and 16.
613
613
614 If the disclaimer of warranty and limitation of liability provided
614 If the disclaimer of warranty and limitation of liability provided
615 above cannot be given local legal effect according to their terms,
615 above cannot be given local legal effect according to their terms,
616 reviewing courts shall apply local law that most closely approximates
616 reviewing courts shall apply local law that most closely approximates
617 an absolute waiver of all civil liability in connection with the
617 an absolute waiver of all civil liability in connection with the
618 Program, unless a warranty or assumption of liability accompanies a
618 Program, unless a warranty or assumption of liability accompanies a
619 copy of the Program in return for a fee.
619 copy of the Program in return for a fee.
620
620
621 END OF TERMS AND CONDITIONS
621 END OF TERMS AND CONDITIONS
622
622
623 How to Apply These Terms to Your New Programs
623 How to Apply These Terms to Your New Programs
624
624
625 If you develop a new program, and you want it to be of the greatest
625 If you develop a new program, and you want it to be of the greatest
626 possible use to the public, the best way to achieve this is to make it
626 possible use to the public, the best way to achieve this is to make it
627 free software which everyone can redistribute and change under these terms.
627 free software which everyone can redistribute and change under these terms.
628
628
629 To do so, attach the following notices to the program. It is safest
629 To do so, attach the following notices to the program. It is safest
630 to attach them to the start of each source file to most effectively
630 to attach them to the start of each source file to most effectively
631 state the exclusion of warranty; and each file should have at least
631 state the exclusion of warranty; and each file should have at least
632 the "copyright" line and a pointer to where the full notice is found.
632 the "copyright" line and a pointer to where the full notice is found.
633
633
634 <one line to give the program's name and a brief idea of what it does.>
634 neboard - a free imageboard
635 Copyright (C) <year> <name of author>
635 Copyright (C) 2013-2014 nekorin
636
636
637 This program is free software: you can redistribute it and/or modify
637 This program is free software: you can redistribute it and/or modify
638 it under the terms of the GNU General Public License as published by
638 it under the terms of the GNU General Public License as published by
639 the Free Software Foundation, either version 3 of the License, or
639 the Free Software Foundation, either version 3 of the License, or
640 (at your option) any later version.
640 (at your option) any later version.
641
641
642 This program is distributed in the hope that it will be useful,
642 This program is distributed in the hope that it will be useful,
643 but WITHOUT ANY WARRANTY; without even the implied warranty of
643 but WITHOUT ANY WARRANTY; without even the implied warranty of
644 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
644 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 GNU General Public License for more details.
645 GNU General Public License for more details.
646
646
647 You should have received a copy of the GNU General Public License
647 You should have received a copy of the GNU General Public License
648 along with this program. If not, see <http://www.gnu.org/licenses/>.
648 along with this program. If not, see <http://www.gnu.org/licenses/>.
649
649
650 Also add information on how to contact you by electronic and paper mail.
650 Also add information on how to contact you by electronic and paper mail.
651
651
652 If the program does terminal interaction, make it output a short
652 If the program does terminal interaction, make it output a short
653 notice like this when it starts in an interactive mode:
653 notice like this when it starts in an interactive mode:
654
654
655 <program> Copyright (C) <year> <name of author>
655 neboard Copyright (C) 2013-2014 nekorin
656 This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
656 This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 This is free software, and you are welcome to redistribute it
657 This is free software, and you are welcome to redistribute it
658 under certain conditions; type `show c' for details.
658 under certain conditions; type `show c' for details.
659
659
660 The hypothetical commands `show w' and `show c' should show the appropriate
660 The hypothetical commands `show w' and `show c' should show the appropriate
661 parts of the General Public License. Of course, your program's commands
661 parts of the General Public License. Of course, your program's commands
662 might be different; for a GUI interface, you would use an "about box".
662 might be different; for a GUI interface, you would use an "about box".
663
663
664 You should also get your employer (if you work as a programmer) or school,
664 You should also get your employer (if you work as a programmer) or school,
665 if any, to sign a "copyright disclaimer" for the program, if necessary.
665 if any, to sign a "copyright disclaimer" for the program, if necessary.
666 For more information on this, and how to apply and follow the GNU GPL, see
666 For more information on this, and how to apply and follow the GNU GPL, see
667 <http://www.gnu.org/licenses/>.
667 <http://www.gnu.org/licenses/>.
668
668
669 The GNU General Public License does not permit incorporating your program
669 The GNU General Public License does not permit incorporating your program
670 into proprietary programs. If your program is a subroutine library, you
670 into proprietary programs. If your program is a subroutine library, you
671 may consider it more useful to permit linking proprietary applications with
671 may consider it more useful to permit linking proprietary applications with
672 the library. If this is what you want to do, use the GNU Lesser General
672 the library. If this is what you want to do, use the GNU Lesser General
673 Public License instead of this License. But first, please read
673 Public License instead of this License. But first, please read
674 <http://www.gnu.org/philosophy/why-not-lgpl.html>.
674 <http://www.gnu.org/philosophy/why-not-lgpl.html>.
@@ -1,259 +1,276 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 markdown_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 )
109 )
109
110
110 MIDDLEWARE_CLASSES = (
111 MIDDLEWARE_CLASSES = (
111 'django.contrib.sessions.middleware.SessionMiddleware',
112 'django.contrib.sessions.middleware.SessionMiddleware',
112 'django.middleware.locale.LocaleMiddleware',
113 'django.middleware.locale.LocaleMiddleware',
113 'django.middleware.common.CommonMiddleware',
114 'django.middleware.common.CommonMiddleware',
114 'django.contrib.auth.middleware.AuthenticationMiddleware',
115 'django.contrib.auth.middleware.AuthenticationMiddleware',
115 'django.contrib.messages.middleware.MessageMiddleware',
116 'django.contrib.messages.middleware.MessageMiddleware',
116 'boards.middlewares.BanMiddleware',
117 'boards.middlewares.BanMiddleware',
117 'boards.middlewares.MinifyHTMLMiddleware',
118 'boards.middlewares.MinifyHTMLMiddleware',
118 )
119 )
119
120
120 ROOT_URLCONF = 'neboard.urls'
121 ROOT_URLCONF = 'neboard.urls'
121
122
122 # 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.
123 WSGI_APPLICATION = 'neboard.wsgi.application'
124 WSGI_APPLICATION = 'neboard.wsgi.application'
124
125
125 TEMPLATE_DIRS = (
126 TEMPLATE_DIRS = (
126 # 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".
127 # Always use forward slashes, even on Windows.
128 # Always use forward slashes, even on Windows.
128 # Don't forget to use absolute paths, not relative paths.
129 # Don't forget to use absolute paths, not relative paths.
129 'templates',
130 'templates',
130 )
131 )
131
132
132 INSTALLED_APPS = (
133 INSTALLED_APPS = (
133 'django.contrib.auth',
134 'django.contrib.auth',
134 'django.contrib.contenttypes',
135 'django.contrib.contenttypes',
135 'django.contrib.sessions',
136 'django.contrib.sessions',
136 # 'django.contrib.sites',
137 # 'django.contrib.sites',
137 'django.contrib.messages',
138 'django.contrib.messages',
138 'django.contrib.staticfiles',
139 'django.contrib.staticfiles',
139 # Uncomment the next line to enable the admin:
140 # Uncomment the next line to enable the admin:
140 'django.contrib.admin',
141 'django.contrib.admin',
141 # Uncomment the next line to enable admin documentation:
142 # Uncomment the next line to enable admin documentation:
142 # 'django.contrib.admindocs',
143 # 'django.contrib.admindocs',
143 'django.contrib.humanize',
144 'django.contrib.humanize',
144 'django_cleanup',
145 'django_cleanup',
145 'boards',
146
146 'captcha',
147 # Migrations
147 'south',
148 'south',
148 'debug_toolbar',
149 'debug_toolbar',
150
151 'captcha',
152
153 # Search
154 'haystack',
155
156 'boards',
149 )
157 )
150
158
151 DEBUG_TOOLBAR_PANELS = (
159 DEBUG_TOOLBAR_PANELS = (
152 'debug_toolbar.panels.version.VersionDebugPanel',
160 'debug_toolbar.panels.version.VersionDebugPanel',
153 'debug_toolbar.panels.timer.TimerDebugPanel',
161 'debug_toolbar.panels.timer.TimerDebugPanel',
154 'debug_toolbar.panels.settings_vars.SettingsVarsDebugPanel',
162 'debug_toolbar.panels.settings_vars.SettingsVarsDebugPanel',
155 'debug_toolbar.panels.headers.HeaderDebugPanel',
163 'debug_toolbar.panels.headers.HeaderDebugPanel',
156 'debug_toolbar.panels.request_vars.RequestVarsDebugPanel',
164 'debug_toolbar.panels.request_vars.RequestVarsDebugPanel',
157 'debug_toolbar.panels.template.TemplateDebugPanel',
165 'debug_toolbar.panels.template.TemplateDebugPanel',
158 'debug_toolbar.panels.sql.SQLDebugPanel',
166 'debug_toolbar.panels.sql.SQLDebugPanel',
159 'debug_toolbar.panels.signals.SignalDebugPanel',
167 'debug_toolbar.panels.signals.SignalDebugPanel',
160 'debug_toolbar.panels.logger.LoggingPanel',
168 'debug_toolbar.panels.logger.LoggingPanel',
161 )
169 )
162
170
163 # TODO: NEED DESIGN FIXES
171 # TODO: NEED DESIGN FIXES
164 CAPTCHA_OUTPUT_FORMAT = (u' %(hidden_field)s '
172 CAPTCHA_OUTPUT_FORMAT = (u' %(hidden_field)s '
165 u'<div class="form-label">%(image)s</div>'
173 u'<div class="form-label">%(image)s</div>'
166 u'<div class="form-text">%(text_field)s</div>')
174 u'<div class="form-text">%(text_field)s</div>')
167
175
168 # A sample logging configuration. The only tangible logging
176 # A sample logging configuration. The only tangible logging
169 # performed by this configuration is to send an email to
177 # performed by this configuration is to send an email to
170 # the site admins on every HTTP 500 error when DEBUG=False.
178 # the site admins on every HTTP 500 error when DEBUG=False.
171 # See http://docs.djangoproject.com/en/dev/topics/logging for
179 # See http://docs.djangoproject.com/en/dev/topics/logging for
172 # more details on how to customize your logging configuration.
180 # more details on how to customize your logging configuration.
173 LOGGING = {
181 LOGGING = {
174 'version': 1,
182 'version': 1,
175 'disable_existing_loggers': False,
183 'disable_existing_loggers': False,
176 'formatters': {
184 'formatters': {
177 'verbose': {
185 'verbose': {
178 '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'
179 },
187 },
180 'simple': {
188 'simple': {
181 'format': '%(levelname)s %(asctime)s [%(module)s] %(message)s'
189 'format': '%(levelname)s %(asctime)s [%(module)s] %(message)s'
182 },
190 },
183 },
191 },
184 'filters': {
192 'filters': {
185 'require_debug_false': {
193 'require_debug_false': {
186 '()': 'django.utils.log.RequireDebugFalse'
194 '()': 'django.utils.log.RequireDebugFalse'
187 }
195 }
188 },
196 },
189 'handlers': {
197 'handlers': {
190 'console': {
198 'console': {
191 'level': 'DEBUG',
199 'level': 'DEBUG',
192 'class': 'logging.StreamHandler',
200 'class': 'logging.StreamHandler',
193 'formatter': 'simple'
201 'formatter': 'simple'
194 },
202 },
195 },
203 },
196 'loggers': {
204 'loggers': {
197 'boards': {
205 'boards': {
198 'handlers': ['console'],
206 'handlers': ['console'],
199 'level': 'DEBUG',
207 'level': 'DEBUG',
200 }
208 }
201 },
209 },
202 }
210 }
203
211
212 HAYSTACK_CONNECTIONS = {
213 'default': {
214 'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine',
215 'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'),
216 },
217 }
218
219 HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
220
204 MARKUP_FIELD_TYPES = (
221 MARKUP_FIELD_TYPES = (
205 ('markdown', markdown_extended),
222 ('markdown', markdown_extended),
206 )
223 )
207 # Custom imageboard settings
224 # Custom imageboard settings
208 # TODO These should me moved to
225 # TODO These should me moved to
209 MAX_POSTS_PER_THREAD = 10 # Thread bumplimit
226 MAX_POSTS_PER_THREAD = 10 # Thread bumplimit
210 MAX_THREAD_COUNT = 5 # Old threads will be deleted to preserve this count
227 MAX_THREAD_COUNT = 5 # Old threads will be deleted to preserve this count
211 THREADS_PER_PAGE = 3
228 THREADS_PER_PAGE = 3
212 SITE_NAME = 'Neboard'
229 SITE_NAME = 'Neboard'
213
230
214 THEMES = [
231 THEMES = [
215 ('md', 'Mystic Dark'),
232 ('md', 'Mystic Dark'),
216 ('md_centered', 'Mystic Dark (centered)'),
233 ('md_centered', 'Mystic Dark (centered)'),
217 ('sw', 'Snow White'),
234 ('sw', 'Snow White'),
218 ('pg', 'Photon Gray'),
235 ('pg', 'Photon Gray'),
219 ]
236 ]
220
237
221 DEFAULT_THEME = 'md'
238 DEFAULT_THEME = 'md'
222
239
223 POPULAR_TAGS = 10
240 POPULAR_TAGS = 10
224 LAST_REPLIES_COUNT = 3
241 LAST_REPLIES_COUNT = 3
225
242
226 ENABLE_CAPTCHA = False
243 ENABLE_CAPTCHA = False
227 # if user tries to post before CAPTCHA_DEFAULT_SAFE_TIME. Captcha will be shown
244 # if user tries to post before CAPTCHA_DEFAULT_SAFE_TIME. Captcha will be shown
228 CAPTCHA_DEFAULT_SAFE_TIME = 30 # seconds
245 CAPTCHA_DEFAULT_SAFE_TIME = 30 # seconds
229 POSTING_DELAY = 20 # seconds
246 POSTING_DELAY = 20 # seconds
230
247
231 COMPRESS_HTML = True
248 COMPRESS_HTML = True
232
249
233 VERSION = '1.7.4 Anubis'
250 VERSION = '1.8.0 Kara'
234
251
235 # Debug mode middlewares
252 # Debug mode middlewares
236 if DEBUG:
253 if DEBUG:
237
254
238 SITE_NAME += ' DEBUG'
255 SITE_NAME += ' DEBUG'
239
256
240 MIDDLEWARE_CLASSES += (
257 MIDDLEWARE_CLASSES += (
241 'boards.profiler.ProfilerMiddleware',
258 'boards.profiler.ProfilerMiddleware',
242 'debug_toolbar.middleware.DebugToolbarMiddleware',
259 'debug_toolbar.middleware.DebugToolbarMiddleware',
243 )
260 )
244
261
245 def custom_show_toolbar(request):
262 def custom_show_toolbar(request):
246 return DEBUG
263 return DEBUG
247
264
248 DEBUG_TOOLBAR_CONFIG = {
265 DEBUG_TOOLBAR_CONFIG = {
249 'INTERCEPT_REDIRECTS': False,
266 'INTERCEPT_REDIRECTS': False,
250 'SHOW_TOOLBAR_CALLBACK': custom_show_toolbar,
267 'SHOW_TOOLBAR_CALLBACK': custom_show_toolbar,
251 'HIDE_DJANGO_SQL': False,
268 'HIDE_DJANGO_SQL': False,
252 'ENABLE_STACKTRACES': True,
269 'ENABLE_STACKTRACES': True,
253 }
270 }
254
271
255 # FIXME Uncommenting this fails somehow. Need to investigate this
272 # FIXME Uncommenting this fails somehow. Need to investigate this
256 #DEBUG_TOOLBAR_PANELS += (
273 #DEBUG_TOOLBAR_PANELS += (
257 # 'debug_toolbar.panels.profiling.ProfilingDebugPanel',
274 # 'debug_toolbar.panels.profiling.ProfilingDebugPanel',
258 #)
275 #)
259
276
@@ -1,8 +1,9 b''
1 haystack
1 pillow
2 pillow
2 django>=1.6
3 django>=1.6
3 django_cleanup
4 django_cleanup
4 django-markupfield
5 django-markupfield
5 markdown
6 markdown
6 python-markdown
7 python-markdown
7 django-simple-captcha
8 django-simple-captcha
8 line-profiler
9 line-profiler
General Comments 0
You need to be logged in to leave comments. Login now