##// END OF EJS Templates
Added fetch sources to fetch external information into threads as posts
neko259 -
r1968:81b9b636 default
parent child Browse files
Show More
@@ -0,0 +1,27 b''
1 # -*- coding: utf-8 -*-
2 # Generated by Django 1.11 on 2017-11-21 11:29
3 from __future__ import unicode_literals
4
5 from django.db import migrations, models
6 import django.db.models.deletion
7
8
9 class Migration(migrations.Migration):
10
11 dependencies = [
12 ('boards', '0066_auto_20171025_1148'),
13 ]
14
15 operations = [
16 migrations.CreateModel(
17 name='ThreadSource',
18 fields=[
19 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
20 ('name', models.TextField()),
21 ('timestamp', models.DateTimeField()),
22 ('source', models.TextField()),
23 ('source_type', models.CharField(choices=[('RSS', 'RSS')], max_length=100)),
24 ('thread', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='boards.Thread')),
25 ],
26 ),
27 ]
@@ -0,0 +1,66 b''
1 import feedparser
2 import logging
3
4 from time import mktime
5 from datetime import datetime
6
7 from django.db import models, transaction
8 from django.utils.dateparse import parse_datetime
9 from django.utils.timezone import utc
10 from django.utils import timezone
11 from boards.models import Post
12 from boards.models.post import TITLE_MAX_LENGTH
13
14
15 SOURCE_TYPE_MAX_LENGTH = 100
16 SOURCE_TYPE_RSS = 'RSS'
17 TYPE_CHOICES = (
18 (SOURCE_TYPE_RSS, SOURCE_TYPE_RSS),
19 )
20
21
22 class ThreadSource(models.Model):
23 class Meta:
24 app_label = 'boards'
25
26 name = models.TextField()
27 thread = models.ForeignKey('Thread')
28 timestamp = models.DateTimeField()
29 source = models.TextField()
30 source_type = models.CharField(max_length=SOURCE_TYPE_MAX_LENGTH,
31 choices=TYPE_CHOICES)
32
33 def __str__(self):
34 return self.name
35
36 @transaction.atomic
37 def fetch_latest_posts(self):
38 """Creates new posts with the info fetched since the timestamp."""
39 logger = logging.getLogger('boards.source')
40
41 if self.thread.is_archived():
42 logger.error('The thread {} is archived, please try another one'.format(self.thread))
43 else:
44 start_timestamp = timezone.localtime(self.timestamp)
45 last_timestamp = start_timestamp
46 if self.thread.is_bumplimit():
47 logger.warn('The thread {} has reached its bumplimit, please create a new one'.format(self.thread))
48 if self.source_type == SOURCE_TYPE_RSS:
49 feed = feedparser.parse(self.source)
50 items = sorted(feed.entries, key=lambda entry: entry.published_parsed)
51 for item in items:
52 title = item.title[:TITLE_MAX_LENGTH]
53 timestamp = datetime.fromtimestamp(mktime(item.published_parsed), tz=utc)
54 if not timestamp:
55 logger.error('Invalid timestamp {} for {}'.format(item.published, title))
56 else:
57 if timestamp > last_timestamp:
58 last_timestamp = timestamp
59
60 if timestamp > start_timestamp:
61 Post.objects.create_post(title=title, text=item.description, thread=self.thread, file_urls=[item.link])
62 logger.info('Fetched item {} from {} into thread {}'.format(
63 title, self.name, self.thread))
64 self.timestamp = last_timestamp
65 self.save(update_fields=['timestamp'])
66
@@ -1,181 +1,188 b''
1 from boards.abstracts.sticker_factory import StickerFactory
1 from boards.abstracts.sticker_factory import StickerFactory
2 from boards.models.attachment import FILE_TYPES_IMAGE, AttachmentSticker, \
2 from boards.models.attachment import FILE_TYPES_IMAGE, AttachmentSticker, \
3 StickerPack
3 StickerPack
4 from django.contrib import admin
4 from django.contrib import admin
5 from django.utils.translation import ugettext_lazy as _
5 from django.utils.translation import ugettext_lazy as _
6 from django.core.urlresolvers import reverse
6 from django.core.urlresolvers import reverse
7 from boards.models import Post, Tag, Ban, Thread, Banner, Attachment, \
7 from boards.models import Post, Tag, Ban, Thread, Banner, Attachment, \
8 KeyPair, GlobalId, TagAlias
8 KeyPair, GlobalId, TagAlias
9 from boards.models.source import ThreadSource
9
10
10
11
11 @admin.register(Post)
12 @admin.register(Post)
12 class PostAdmin(admin.ModelAdmin):
13 class PostAdmin(admin.ModelAdmin):
13
14
14 list_display = ('id', 'title', 'text', 'poster_ip', 'linked_images',
15 list_display = ('id', 'title', 'text', 'poster_ip', 'linked_images',
15 'foreign', 'tags')
16 'foreign', 'tags')
16 list_filter = ('pub_time',)
17 list_filter = ('pub_time',)
17 search_fields = ('id', 'title', 'text', 'poster_ip')
18 search_fields = ('id', 'title', 'text', 'poster_ip')
18 exclude = ('referenced_posts', 'refmap', 'images', 'global_id')
19 exclude = ('referenced_posts', 'refmap', 'images', 'global_id')
19 readonly_fields = ('poster_ip', 'thread', 'linked_images',
20 readonly_fields = ('poster_ip', 'thread', 'linked_images',
20 'attachments', 'uid', 'url', 'pub_time', 'opening', 'linked_global_id',
21 'attachments', 'uid', 'url', 'pub_time', 'opening', 'linked_global_id',
21 'foreign', 'tags')
22 'foreign', 'tags')
22
23
23 def ban_poster(self, request, queryset):
24 def ban_poster(self, request, queryset):
24 bans = 0
25 bans = 0
25 for post in queryset:
26 for post in queryset:
26 poster_ip = post.poster_ip
27 poster_ip = post.poster_ip
27 ban, created = Ban.objects.get_or_create(ip=poster_ip)
28 ban, created = Ban.objects.get_or_create(ip=poster_ip)
28 if created:
29 if created:
29 bans += 1
30 bans += 1
30 self.message_user(request, _('{} posters were banned').format(bans))
31 self.message_user(request, _('{} posters were banned').format(bans))
31
32
32 def ban_latter_with_delete(self, request, queryset):
33 def ban_latter_with_delete(self, request, queryset):
33 bans = 0
34 bans = 0
34 hidden = 0
35 hidden = 0
35 for post in queryset:
36 for post in queryset:
36 poster_ip = post.poster_ip
37 poster_ip = post.poster_ip
37 ban, created = Ban.objects.get_or_create(ip=poster_ip)
38 ban, created = Ban.objects.get_or_create(ip=poster_ip)
38 if created:
39 if created:
39 bans += 1
40 bans += 1
40 posts = Post.objects.filter(poster_ip=poster_ip, id__gte=post.id)
41 posts = Post.objects.filter(poster_ip=poster_ip, id__gte=post.id)
41 hidden += posts.count()
42 hidden += posts.count()
42 posts.delete()
43 posts.delete()
43 self.message_user(request, _('{} posters were banned, {} messages were removed.').format(bans, hidden))
44 self.message_user(request, _('{} posters were banned, {} messages were removed.').format(bans, hidden))
44 ban_latter_with_delete.short_description = _('Ban user and delete posts starting from this one and later')
45 ban_latter_with_delete.short_description = _('Ban user and delete posts starting from this one and later')
45
46
46 def linked_images(self, obj: Post):
47 def linked_images(self, obj: Post):
47 images = obj.attachments.filter(mimetype__in=FILE_TYPES_IMAGE)
48 images = obj.attachments.filter(mimetype__in=FILE_TYPES_IMAGE)
48 image_urls = ['<a href="{}"><img src="{}" /></a>'.format(
49 image_urls = ['<a href="{}"><img src="{}" /></a>'.format(
49 reverse('admin:%s_%s_change' % (image._meta.app_label,
50 reverse('admin:%s_%s_change' % (image._meta.app_label,
50 image._meta.model_name),
51 image._meta.model_name),
51 args=[image.id]), image.get_thumb_url()) for image in images]
52 args=[image.id]), image.get_thumb_url()) for image in images]
52 return ', '.join(image_urls)
53 return ', '.join(image_urls)
53 linked_images.allow_tags = True
54 linked_images.allow_tags = True
54
55
55 def linked_global_id(self, obj: Post):
56 def linked_global_id(self, obj: Post):
56 global_id = obj.global_id
57 global_id = obj.global_id
57 if global_id is not None:
58 if global_id is not None:
58 return '<a href="{}">{}</a>'.format(
59 return '<a href="{}">{}</a>'.format(
59 reverse('admin:%s_%s_change' % (global_id._meta.app_label,
60 reverse('admin:%s_%s_change' % (global_id._meta.app_label,
60 global_id._meta.model_name),
61 global_id._meta.model_name),
61 args=[global_id.id]), str(global_id))
62 args=[global_id.id]), str(global_id))
62 linked_global_id.allow_tags = True
63 linked_global_id.allow_tags = True
63
64
64 def tags(self, obj: Post):
65 def tags(self, obj: Post):
65 return ', '.join([tag.get_name() for tag in obj.get_tags()])
66 return ', '.join([tag.get_name() for tag in obj.get_tags()])
66
67
67 def save_model(self, request, obj, form, change):
68 def save_model(self, request, obj, form, change):
68 obj.save()
69 obj.save()
69 obj.clear_cache()
70 obj.clear_cache()
70
71
71 def foreign(self, obj: Post):
72 def foreign(self, obj: Post):
72 return obj is not None and obj.global_id is not None and\
73 return obj is not None and obj.global_id is not None and\
73 not obj.global_id.is_local()
74 not obj.global_id.is_local()
74
75
75 actions = ['ban_poster', 'ban_latter_with_delete']
76 actions = ['ban_poster', 'ban_latter_with_delete']
76
77
77
78
78 @admin.register(Tag)
79 @admin.register(Tag)
79 class TagAdmin(admin.ModelAdmin):
80 class TagAdmin(admin.ModelAdmin):
80 def thread_count(self, obj: Tag) -> int:
81 def thread_count(self, obj: Tag) -> int:
81 return obj.get_thread_count()
82 return obj.get_thread_count()
82
83
83 def display_children(self, obj: Tag):
84 def display_children(self, obj: Tag):
84 return ', '.join([str(child) for child in obj.get_children().all()])
85 return ', '.join([str(child) for child in obj.get_children().all()])
85
86
86 def name(self, obj: Tag):
87 def name(self, obj: Tag):
87 return obj.get_name()
88 return obj.get_name()
88
89
89 def save_model(self, request, obj, form, change):
90 def save_model(self, request, obj, form, change):
90 super().save_model(request, obj, form, change)
91 super().save_model(request, obj, form, change)
91 for thread in obj.get_threads().all():
92 for thread in obj.get_threads().all():
92 thread.refresh_tags()
93 thread.refresh_tags()
93
94
94 list_display = ('name', 'thread_count', 'display_children')
95 list_display = ('name', 'thread_count', 'display_children')
95 search_fields = ('id',)
96 search_fields = ('id',)
96 readonly_fields = ('name',)
97 readonly_fields = ('name',)
97
98
98
99
99 @admin.register(TagAlias)
100 @admin.register(TagAlias)
100 class TagAliasAdmin(admin.ModelAdmin):
101 class TagAliasAdmin(admin.ModelAdmin):
101 list_display = ('locale', 'name', 'parent')
102 list_display = ('locale', 'name', 'parent')
102 list_filter = ('locale',)
103 list_filter = ('locale',)
103 search_fields = ('name',)
104 search_fields = ('name',)
104
105
105
106
106 @admin.register(Thread)
107 @admin.register(Thread)
107 class ThreadAdmin(admin.ModelAdmin):
108 class ThreadAdmin(admin.ModelAdmin):
108
109
109 def title(self, obj: Thread) -> str:
110 def title(self, obj: Thread) -> str:
110 return obj.get_opening_post().get_title()
111 return obj.get_opening_post().get_title()
111
112
112 def reply_count(self, obj: Thread) -> int:
113 def reply_count(self, obj: Thread) -> int:
113 return obj.get_reply_count()
114 return obj.get_reply_count()
114
115
115 def ip(self, obj: Thread):
116 def ip(self, obj: Thread):
116 return obj.get_opening_post().poster_ip
117 return obj.get_opening_post().poster_ip
117
118
118 def display_tags(self, obj: Thread):
119 def display_tags(self, obj: Thread):
119 return ', '.join([str(tag) for tag in obj.get_tags().all()])
120 return ', '.join([str(tag) for tag in obj.get_tags().all()])
120
121
121 def op(self, obj: Thread):
122 def op(self, obj: Thread):
122 return obj.get_opening_post_id()
123 return obj.get_opening_post_id()
123
124
124 # Save parent tags when editing tags
125 # Save parent tags when editing tags
125 def save_related(self, request, form, formsets, change):
126 def save_related(self, request, form, formsets, change):
126 super().save_related(request, form, formsets, change)
127 super().save_related(request, form, formsets, change)
127 form.instance.refresh_tags()
128 form.instance.refresh_tags()
128
129
129 def save_model(self, request, obj, form, change):
130 def save_model(self, request, obj, form, change):
130 op = obj.get_opening_post()
131 op = obj.get_opening_post()
131 obj.save()
132 obj.save()
132 op.clear_cache()
133 op.clear_cache()
133
134
134 list_display = ('id', 'op', 'title', 'reply_count', 'status', 'ip',
135 list_display = ('id', 'op', 'title', 'reply_count', 'status', 'ip',
135 'display_tags')
136 'display_tags')
136 list_filter = ('bump_time', 'status')
137 list_filter = ('bump_time', 'status')
137 search_fields = ('id', 'title')
138 search_fields = ('id', 'title')
138 filter_horizontal = ('tags',)
139 filter_horizontal = ('tags',)
139
140
140
141
141 @admin.register(KeyPair)
142 @admin.register(KeyPair)
142 class KeyPairAdmin(admin.ModelAdmin):
143 class KeyPairAdmin(admin.ModelAdmin):
143 list_display = ('public_key', 'primary')
144 list_display = ('public_key', 'primary')
144 list_filter = ('primary',)
145 list_filter = ('primary',)
145 search_fields = ('public_key',)
146 search_fields = ('public_key',)
146
147
147
148
148 @admin.register(Ban)
149 @admin.register(Ban)
149 class BanAdmin(admin.ModelAdmin):
150 class BanAdmin(admin.ModelAdmin):
150 list_display = ('ip', 'can_read')
151 list_display = ('ip', 'can_read')
151 list_filter = ('can_read',)
152 list_filter = ('can_read',)
152 search_fields = ('ip',)
153 search_fields = ('ip',)
153
154
154
155
155 @admin.register(Banner)
156 @admin.register(Banner)
156 class BannerAdmin(admin.ModelAdmin):
157 class BannerAdmin(admin.ModelAdmin):
157 list_display = ('title', 'text')
158 list_display = ('title', 'text')
158
159
159
160
160 @admin.register(Attachment)
161 @admin.register(Attachment)
161 class AttachmentAdmin(admin.ModelAdmin):
162 class AttachmentAdmin(admin.ModelAdmin):
162 list_display = ('__str__', 'mimetype', 'file', 'url')
163 list_display = ('__str__', 'mimetype', 'file', 'url')
163
164
164
165
165 @admin.register(AttachmentSticker)
166 @admin.register(AttachmentSticker)
166 class AttachmentStickerAdmin(admin.ModelAdmin):
167 class AttachmentStickerAdmin(admin.ModelAdmin):
167 search_fields = ('name',)
168 search_fields = ('name',)
168
169
169
170
170 @admin.register(StickerPack)
171 @admin.register(StickerPack)
171 class StickerPackAdmin(admin.ModelAdmin):
172 class StickerPackAdmin(admin.ModelAdmin):
172 search_fields = ('name',)
173 search_fields = ('name',)
173
174
174
175
175 @admin.register(GlobalId)
176 @admin.register(GlobalId)
176 class GlobalIdAdmin(admin.ModelAdmin):
177 class GlobalIdAdmin(admin.ModelAdmin):
177 def is_linked(self, obj):
178 def is_linked(self, obj):
178 return Post.objects.filter(global_id=obj).exists()
179 return Post.objects.filter(global_id=obj).exists()
179
180
180 list_display = ('__str__', 'is_linked',)
181 list_display = ('__str__', 'is_linked',)
181 readonly_fields = ('content',)
182 readonly_fields = ('content',)
183
184
185 @admin.register(ThreadSource)
186 class ThreadSourceAdmin(admin.ModelAdmin):
187 search_fields = ('name', 'source')
188
@@ -1,49 +1,50 b''
1 [Version]
1 [Version]
2 Version = 4.6.0 Lucy
2 Version = 4.6.0 Lucy
3 SiteName = Neboard DEV
3 SiteName = Neboard DEV
4
4
5 [Cache]
5 [Cache]
6 # Timeout for caching, if cache is used
6 # Timeout for caching, if cache is used
7 CacheTimeout = 600
7 CacheTimeout = 600
8
8
9 [Forms]
9 [Forms]
10 # Max post length in characters
10 # Max post length in characters
11 MaxTextLength = 30000
11 MaxTextLength = 30000
12 MaxFileSize = 8000000
12 MaxFileSize = 8000000
13 LimitFirstPosting = true
13 LimitFirstPosting = true
14 LimitPostingSpeed = false
14 LimitPostingSpeed = false
15 PowDifficulty = 0
15 PowDifficulty = 0
16 # Delay in seconds
16 # Delay in seconds
17 PostingDelay = 30
17 PostingDelay = 30
18 Autoban = false
18 Autoban = false
19 DefaultTag = test
19 DefaultTag = test
20 MaxFileCount = 1
20 MaxFileCount = 1
21 AdditionalSpoilerSpaces = false
21 AdditionalSpoilerSpaces = false
22
22
23 [Messages]
23 [Messages]
24 # Thread bumplimit
24 # Thread bumplimit
25 MaxPostsPerThread = 10
25 MaxPostsPerThread = 10
26 ThreadArchiveDays = 300
26 ThreadArchiveDays = 300
27 AnonymousMode = false
27 AnonymousMode = false
28
28
29 [View]
29 [View]
30 DefaultTheme = md
30 DefaultTheme = md
31 DefaultImageViewer = simple
31 DefaultImageViewer = simple
32 LastRepliesCount = 3
32 LastRepliesCount = 3
33 ThreadsPerPage = 3
33 ThreadsPerPage = 3
34 PostsPerPage = 10
34 PostsPerPage = 10
35 ImagesPerPageGallery = 20
35 ImagesPerPageGallery = 20
36 MaxFavoriteThreads = 20
36 MaxFavoriteThreads = 20
37 MaxLandingThreads = 20
37 MaxLandingThreads = 20
38 Themes=md:Mystic Dark,md_centered:Mystic Dark (centered),sw:Snow White,pg:Photon Grey,ad:Amanita Dark,iw:Inocibe White
38 Themes=md:Mystic Dark,md_centered:Mystic Dark (centered),sw:Snow White,pg:Photon Grey,ad:Amanita Dark,iw:Inocibe White
39 ImageViewers=simple:Simple,popup:Popup
39 ImageViewers=simple:Simple,popup:Popup
40
40
41 [Storage]
41 [Storage]
42 # Enable archiving threads instead of deletion when the thread limit is reached
42 # Enable archiving threads instead of deletion when the thread limit is reached
43 ArchiveThreads = true
43 ArchiveThreads = true
44
44
45 [RSS]
45 [RSS]
46 MaxItems = 20
46 MaxItems = 20
47
47
48 [External]
48 [External]
49 ImageSearchHost=
49 ImageSearchHost=
50 SourceFetcherTripcode=
@@ -1,322 +1,325 b''
1 import logging
1 import logging
2 from datetime import timedelta
2 from datetime import timedelta
3
3
4 from django.db import models, transaction
4 from django.db import models, transaction
5 from django.db.models import Count, Sum, QuerySet, Q
5 from django.db.models import Count, Sum, QuerySet, Q
6 from django.utils import timezone
6 from django.utils import timezone
7
7
8 import boards
8 import boards
9 from boards import settings
9 from boards import settings
10 from boards.models import STATUS_BUMPLIMIT, STATUS_ACTIVE, STATUS_ARCHIVE
10 from boards.models import STATUS_BUMPLIMIT, STATUS_ACTIVE, STATUS_ARCHIVE
11 from boards.models.attachment import FILE_TYPES_IMAGE
11 from boards.models.attachment import FILE_TYPES_IMAGE
12 from boards.models.post import Post
12 from boards.models.post import Post
13 from boards.models.tag import Tag, DEFAULT_LOCALE, TagAlias
13 from boards.models.tag import Tag, DEFAULT_LOCALE, TagAlias
14 from boards.utils import cached_result, datetime_to_epoch
14 from boards.utils import cached_result, datetime_to_epoch
15
15
16 FAV_THREAD_NO_UPDATES = -1
16 FAV_THREAD_NO_UPDATES = -1
17
17
18
18
19 __author__ = 'neko259'
19 __author__ = 'neko259'
20
20
21
21
22 logger = logging.getLogger(__name__)
22 logger = logging.getLogger(__name__)
23
23
24
24
25 WS_NOTIFICATION_TYPE_NEW_POST = 'new_post'
25 WS_NOTIFICATION_TYPE_NEW_POST = 'new_post'
26 WS_NOTIFICATION_TYPE = 'notification_type'
26 WS_NOTIFICATION_TYPE = 'notification_type'
27
27
28 WS_CHANNEL_THREAD = "thread:"
28 WS_CHANNEL_THREAD = "thread:"
29
29
30 STATUS_CHOICES = (
30 STATUS_CHOICES = (
31 (STATUS_ACTIVE, STATUS_ACTIVE),
31 (STATUS_ACTIVE, STATUS_ACTIVE),
32 (STATUS_BUMPLIMIT, STATUS_BUMPLIMIT),
32 (STATUS_BUMPLIMIT, STATUS_BUMPLIMIT),
33 (STATUS_ARCHIVE, STATUS_ARCHIVE),
33 (STATUS_ARCHIVE, STATUS_ARCHIVE),
34 )
34 )
35
35
36
36
37 class ThreadManager(models.Manager):
37 class ThreadManager(models.Manager):
38 def process_old_threads(self):
38 def process_old_threads(self):
39 """
39 """
40 Preserves maximum thread count. If there are too many threads,
40 Preserves maximum thread count. If there are too many threads,
41 archive or delete the old ones.
41 archive or delete the old ones.
42 """
42 """
43 old_time_delta = settings.get_int('Messages', 'ThreadArchiveDays')
43 old_time_delta = settings.get_int('Messages', 'ThreadArchiveDays')
44 old_time = timezone.now() - timedelta(days=old_time_delta)
44 old_time = timezone.now() - timedelta(days=old_time_delta)
45 old_ops = Post.objects.filter(opening=True, pub_time__lte=old_time).exclude(thread__status=STATUS_ARCHIVE)
45 old_ops = Post.objects.filter(opening=True, pub_time__lte=old_time).exclude(thread__status=STATUS_ARCHIVE)
46
46
47 for op in old_ops:
47 for op in old_ops:
48 thread = op.get_thread()
48 thread = op.get_thread()
49 if settings.get_bool('Storage', 'ArchiveThreads'):
49 if settings.get_bool('Storage', 'ArchiveThreads'):
50 self._archive_thread(thread)
50 self._archive_thread(thread)
51 else:
51 else:
52 thread.delete()
52 thread.delete()
53 logger.info('Processed old thread {}'.format(thread))
53 logger.info('Processed old thread {}'.format(thread))
54
54
55
55
56 def _archive_thread(self, thread):
56 def _archive_thread(self, thread):
57 thread.status = STATUS_ARCHIVE
57 thread.status = STATUS_ARCHIVE
58 thread.last_edit_time = timezone.now()
58 thread.last_edit_time = timezone.now()
59 thread.update_posts_time()
59 thread.update_posts_time()
60 thread.save(update_fields=['last_edit_time', 'status'])
60 thread.save(update_fields=['last_edit_time', 'status'])
61
61
62 def get_new_posts(self, datas):
62 def get_new_posts(self, datas):
63 query = None
63 query = None
64 # TODO Use classes instead of dicts
64 # TODO Use classes instead of dicts
65 for data in datas:
65 for data in datas:
66 if data['last_id'] != FAV_THREAD_NO_UPDATES:
66 if data['last_id'] != FAV_THREAD_NO_UPDATES:
67 q = (Q(id=data['op'].get_thread_id())
67 q = (Q(id=data['op'].get_thread_id())
68 & Q(replies__id__gt=data['last_id']))
68 & Q(replies__id__gt=data['last_id']))
69 if query is None:
69 if query is None:
70 query = q
70 query = q
71 else:
71 else:
72 query = query | q
72 query = query | q
73 if query is not None:
73 if query is not None:
74 return self.filter(query).annotate(
74 return self.filter(query).annotate(
75 new_post_count=Count('replies'))
75 new_post_count=Count('replies'))
76
76
77 def get_new_post_count(self, datas):
77 def get_new_post_count(self, datas):
78 new_posts = self.get_new_posts(datas)
78 new_posts = self.get_new_posts(datas)
79 return new_posts.aggregate(total_count=Count('replies'))\
79 return new_posts.aggregate(total_count=Count('replies'))\
80 ['total_count'] if new_posts else 0
80 ['total_count'] if new_posts else 0
81
81
82
82
83 def get_thread_max_posts():
83 def get_thread_max_posts():
84 return settings.get_int('Messages', 'MaxPostsPerThread')
84 return settings.get_int('Messages', 'MaxPostsPerThread')
85
85
86
86
87 class Thread(models.Model):
87 class Thread(models.Model):
88 objects = ThreadManager()
88 objects = ThreadManager()
89
89
90 class Meta:
90 class Meta:
91 app_label = 'boards'
91 app_label = 'boards'
92
92
93 tags = models.ManyToManyField('Tag', related_name='thread_tags')
93 tags = models.ManyToManyField('Tag', related_name='thread_tags')
94 bump_time = models.DateTimeField(db_index=True)
94 bump_time = models.DateTimeField(db_index=True)
95 last_edit_time = models.DateTimeField()
95 last_edit_time = models.DateTimeField()
96 max_posts = models.IntegerField(default=get_thread_max_posts)
96 max_posts = models.IntegerField(default=get_thread_max_posts)
97 status = models.CharField(max_length=50, default=STATUS_ACTIVE,
97 status = models.CharField(max_length=50, default=STATUS_ACTIVE,
98 choices=STATUS_CHOICES, db_index=True)
98 choices=STATUS_CHOICES, db_index=True)
99 monochrome = models.BooleanField(default=False)
99 monochrome = models.BooleanField(default=False)
100 stickerpack = models.BooleanField(default=False)
100 stickerpack = models.BooleanField(default=False)
101
101
102 def get_tags(self) -> QuerySet:
102 def get_tags(self) -> QuerySet:
103 """
103 """
104 Gets a sorted tag list.
104 Gets a sorted tag list.
105 """
105 """
106
106
107 return self.tags.filter(aliases__in=TagAlias.objects.filter_localized(parent__thread_tags=self)).order_by('aliases__name')
107 return self.tags.filter(aliases__in=TagAlias.objects.filter_localized(parent__thread_tags=self)).order_by('aliases__name')
108
108
109 def bump(self):
109 def bump(self):
110 """
110 """
111 Bumps (moves to up) thread if possible.
111 Bumps (moves to up) thread if possible.
112 """
112 """
113
113
114 if self.can_bump():
114 if self.can_bump():
115 self.bump_time = self.last_edit_time
115 self.bump_time = self.last_edit_time
116
116
117 self.update_bump_status()
117 self.update_bump_status()
118
118
119 logger.info('Bumped thread %d' % self.id)
119 logger.info('Bumped thread %d' % self.id)
120
120
121 def has_post_limit(self) -> bool:
121 def has_post_limit(self) -> bool:
122 return self.max_posts > 0
122 return self.max_posts > 0
123
123
124 def update_bump_status(self, exclude_posts=None):
124 def update_bump_status(self, exclude_posts=None):
125 if self.has_post_limit() and self.get_reply_count() >= self.max_posts:
125 if self.has_post_limit() and self.get_reply_count() >= self.max_posts:
126 self.status = STATUS_BUMPLIMIT
126 self.status = STATUS_BUMPLIMIT
127 self.update_posts_time(exclude_posts=exclude_posts)
127 self.update_posts_time(exclude_posts=exclude_posts)
128
128
129 def _get_cache_key(self):
129 def _get_cache_key(self):
130 return [datetime_to_epoch(self.last_edit_time)]
130 return [datetime_to_epoch(self.last_edit_time)]
131
131
132 @cached_result(key_method=_get_cache_key)
132 @cached_result(key_method=_get_cache_key)
133 def get_reply_count(self) -> int:
133 def get_reply_count(self) -> int:
134 return self.get_replies().count()
134 return self.get_replies().count()
135
135
136 @cached_result(key_method=_get_cache_key)
136 @cached_result(key_method=_get_cache_key)
137 def get_images_count(self) -> int:
137 def get_images_count(self) -> int:
138 return self.get_replies().filter(
138 return self.get_replies().filter(
139 attachments__mimetype__in=FILE_TYPES_IMAGE)\
139 attachments__mimetype__in=FILE_TYPES_IMAGE)\
140 .annotate(images_count=Count(
140 .annotate(images_count=Count(
141 'attachments')).aggregate(Sum('images_count'))['images_count__sum'] or 0
141 'attachments')).aggregate(Sum('images_count'))['images_count__sum'] or 0
142
142
143 @cached_result(key_method=_get_cache_key)
143 @cached_result(key_method=_get_cache_key)
144 def get_attachment_count(self) -> int:
144 def get_attachment_count(self) -> int:
145 return self.get_replies().annotate(attachment_count=Count('attachments'))\
145 return self.get_replies().annotate(attachment_count=Count('attachments'))\
146 .aggregate(Sum('attachment_count'))['attachment_count__sum'] or 0
146 .aggregate(Sum('attachment_count'))['attachment_count__sum'] or 0
147
147
148 def can_bump(self) -> bool:
148 def can_bump(self) -> bool:
149 """
149 """
150 Checks if the thread can be bumped by replying to it.
150 Checks if the thread can be bumped by replying to it.
151 """
151 """
152
152
153 return self.get_status() == STATUS_ACTIVE
153 return self.get_status() == STATUS_ACTIVE
154
154
155 def get_last_replies(self) -> QuerySet:
155 def get_last_replies(self) -> QuerySet:
156 """
156 """
157 Gets several last replies, not including opening post
157 Gets several last replies, not including opening post
158 """
158 """
159
159
160 last_replies_count = settings.get_int('View', 'LastRepliesCount')
160 last_replies_count = settings.get_int('View', 'LastRepliesCount')
161
161
162 if last_replies_count > 0:
162 if last_replies_count > 0:
163 reply_count = self.get_reply_count()
163 reply_count = self.get_reply_count()
164
164
165 if reply_count > 0:
165 if reply_count > 0:
166 reply_count_to_show = min(last_replies_count,
166 reply_count_to_show = min(last_replies_count,
167 reply_count - 1)
167 reply_count - 1)
168 replies = self.get_replies()
168 replies = self.get_replies()
169 last_replies = replies[reply_count - reply_count_to_show:]
169 last_replies = replies[reply_count - reply_count_to_show:]
170
170
171 return last_replies
171 return last_replies
172
172
173 def get_skipped_replies_count(self) -> int:
173 def get_skipped_replies_count(self) -> int:
174 """
174 """
175 Gets number of posts between opening post and last replies.
175 Gets number of posts between opening post and last replies.
176 """
176 """
177 reply_count = self.get_reply_count()
177 reply_count = self.get_reply_count()
178 last_replies_count = min(settings.get_int('View', 'LastRepliesCount'),
178 last_replies_count = min(settings.get_int('View', 'LastRepliesCount'),
179 reply_count - 1)
179 reply_count - 1)
180 return reply_count - last_replies_count - 1
180 return reply_count - last_replies_count - 1
181
181
182 # TODO Remove argument, it is not used
182 # TODO Remove argument, it is not used
183 def get_replies(self, view_fields_only=True) -> QuerySet:
183 def get_replies(self, view_fields_only=True) -> QuerySet:
184 """
184 """
185 Gets sorted thread posts
185 Gets sorted thread posts
186 """
186 """
187 query = self.replies.order_by('pub_time').prefetch_related(
187 query = self.replies.order_by('pub_time').prefetch_related(
188 'attachments')
188 'attachments')
189 return query
189 return query
190
190
191 def get_viewable_replies(self) -> QuerySet:
191 def get_viewable_replies(self) -> QuerySet:
192 """
192 """
193 Gets replies with only fields that are used for viewing.
193 Gets replies with only fields that are used for viewing.
194 """
194 """
195 return self.get_replies().defer('text', 'last_edit_time')
195 return self.get_replies().defer('text', 'last_edit_time')
196
196
197 def get_top_level_replies(self) -> QuerySet:
197 def get_top_level_replies(self) -> QuerySet:
198 return self.get_replies().exclude(refposts__threads__in=[self])
198 return self.get_replies().exclude(refposts__threads__in=[self])
199
199
200 def get_replies_with_images(self, view_fields_only=False) -> QuerySet:
200 def get_replies_with_images(self, view_fields_only=False) -> QuerySet:
201 """
201 """
202 Gets replies that have at least one image attached
202 Gets replies that have at least one image attached
203 """
203 """
204 return self.get_replies(view_fields_only).filter(
204 return self.get_replies(view_fields_only).filter(
205 attachments__mimetype__in=FILE_TYPES_IMAGE).annotate(images_count=Count(
205 attachments__mimetype__in=FILE_TYPES_IMAGE).annotate(images_count=Count(
206 'attachments')).filter(images_count__gt=0)
206 'attachments')).filter(images_count__gt=0)
207
207
208 def get_opening_post(self, only_id=False) -> Post:
208 def get_opening_post(self, only_id=False) -> Post:
209 """
209 """
210 Gets the first post of the thread
210 Gets the first post of the thread
211 """
211 """
212
212
213 query = self.get_replies().filter(opening=True)
213 query = self.get_replies().filter(opening=True)
214 if only_id:
214 if only_id:
215 query = query.only('id')
215 query = query.only('id')
216 opening_post = query.first()
216 opening_post = query.first()
217
217
218 return opening_post
218 return opening_post
219
219
220 @cached_result()
220 @cached_result()
221 def get_opening_post_id(self) -> int:
221 def get_opening_post_id(self) -> int:
222 """
222 """
223 Gets ID of the first thread post.
223 Gets ID of the first thread post.
224 """
224 """
225
225
226 return self.get_opening_post(only_id=True).id
226 return self.get_opening_post(only_id=True).id
227
227
228 def get_pub_time(self):
228 def get_pub_time(self):
229 """
229 """
230 Gets opening post's pub time because thread does not have its own one.
230 Gets opening post's pub time because thread does not have its own one.
231 """
231 """
232
232
233 return self.get_opening_post().pub_time
233 return self.get_opening_post().pub_time
234
234
235 def __str__(self):
235 def __str__(self):
236 return 'T#{}'.format(self.id)
236 return 'T#{}/{}'.format(self.id, self.get_opening_post())
237
237
238 def get_tag_url_list(self) -> list:
238 def get_tag_url_list(self) -> list:
239 return boards.models.Tag.objects.get_tag_url_list(self.get_tags().all())
239 return boards.models.Tag.objects.get_tag_url_list(self.get_tags().all())
240
240
241 def update_posts_time(self, exclude_posts=None):
241 def update_posts_time(self, exclude_posts=None):
242 last_edit_time = self.last_edit_time
242 last_edit_time = self.last_edit_time
243
243
244 for post in self.replies.all():
244 for post in self.replies.all():
245 if exclude_posts is None or post not in exclude_posts:
245 if exclude_posts is None or post not in exclude_posts:
246 # Manual update is required because uids are generated on save
246 # Manual update is required because uids are generated on save
247 post.last_edit_time = last_edit_time
247 post.last_edit_time = last_edit_time
248 post.save(update_fields=['last_edit_time'])
248 post.save(update_fields=['last_edit_time'])
249
249
250 def get_absolute_url(self):
250 def get_absolute_url(self):
251 return self.get_opening_post().get_absolute_url()
251 return self.get_opening_post().get_absolute_url()
252
252
253 def get_required_tags(self):
253 def get_required_tags(self):
254 return self.get_tags().filter(required=True)
254 return self.get_tags().filter(required=True)
255
255
256 def get_sections_str(self):
256 def get_sections_str(self):
257 return Tag.objects.get_tag_url_list(self.get_required_tags())
257 return Tag.objects.get_tag_url_list(self.get_required_tags())
258
258
259 def get_replies_newer(self, post_id):
259 def get_replies_newer(self, post_id):
260 return self.get_replies().filter(id__gt=post_id)
260 return self.get_replies().filter(id__gt=post_id)
261
261
262 def is_archived(self):
262 def is_archived(self):
263 return self.get_status() == STATUS_ARCHIVE
263 return self.get_status() == STATUS_ARCHIVE
264
264
265 def is_bumplimit(self):
266 return self.get_status() == STATUS_BUMPLIMIT
267
265 def get_status(self):
268 def get_status(self):
266 return self.status
269 return self.status
267
270
268 def is_monochrome(self):
271 def is_monochrome(self):
269 return self.monochrome
272 return self.monochrome
270
273
271 def is_stickerpack(self):
274 def is_stickerpack(self):
272 return self.stickerpack
275 return self.stickerpack
273
276
274 # If tags have parent, add them to the tag list
277 # If tags have parent, add them to the tag list
275 @transaction.atomic
278 @transaction.atomic
276 def refresh_tags(self):
279 def refresh_tags(self):
277 for tag in self.get_tags().all():
280 for tag in self.get_tags().all():
278 parents = tag.get_all_parents()
281 parents = tag.get_all_parents()
279 if len(parents) > 0:
282 if len(parents) > 0:
280 self.tags.add(*parents)
283 self.tags.add(*parents)
281
284
282 def get_reply_tree(self):
285 def get_reply_tree(self):
283 replies = self.get_replies().prefetch_related('refposts')
286 replies = self.get_replies().prefetch_related('refposts')
284 tree = []
287 tree = []
285 for reply in replies:
288 for reply in replies:
286 parents = reply.refposts.all()
289 parents = reply.refposts.all()
287
290
288 found_parent = False
291 found_parent = False
289 searching_for_index = False
292 searching_for_index = False
290
293
291 if len(parents) > 0:
294 if len(parents) > 0:
292 index = 0
295 index = 0
293 parent_depth = 0
296 parent_depth = 0
294
297
295 indexes_to_insert = []
298 indexes_to_insert = []
296
299
297 for depth, element in tree:
300 for depth, element in tree:
298 index += 1
301 index += 1
299
302
300 # If this element is next after parent on the same level,
303 # If this element is next after parent on the same level,
301 # insert child before it
304 # insert child before it
302 if searching_for_index and depth <= parent_depth:
305 if searching_for_index and depth <= parent_depth:
303 indexes_to_insert.append((index - 1, parent_depth))
306 indexes_to_insert.append((index - 1, parent_depth))
304 searching_for_index = False
307 searching_for_index = False
305
308
306 if element in parents:
309 if element in parents:
307 found_parent = True
310 found_parent = True
308 searching_for_index = True
311 searching_for_index = True
309 parent_depth = depth
312 parent_depth = depth
310
313
311 if not found_parent:
314 if not found_parent:
312 tree.append((0, reply))
315 tree.append((0, reply))
313 else:
316 else:
314 if searching_for_index:
317 if searching_for_index:
315 tree.append((parent_depth + 1, reply))
318 tree.append((parent_depth + 1, reply))
316
319
317 offset = 0
320 offset = 0
318 for last_index, parent_depth in indexes_to_insert:
321 for last_index, parent_depth in indexes_to_insert:
319 tree.insert(last_index + offset, (parent_depth + 1, reply))
322 tree.insert(last_index + offset, (parent_depth + 1, reply))
320 offset += 1
323 offset += 1
321
324
322 return tree
325 return tree
@@ -1,24 +1,29 b''
1 import configparser
1 import configparser
2
2
3
3
4 CONFIG_DEFAULT_SETTINGS = 'boards/config/default_settings.ini'
5 CONFIG_SETTINGS = 'boards/config/settings.ini'
6
7
4 config = configparser.ConfigParser()
8 config = configparser.ConfigParser()
5 config.read('boards/config/default_settings.ini')
9 config.read(CONFIG_DEFAULT_SETTINGS)
6 config.read('boards/config/settings.ini')
10 config.read(CONFIG_SETTINGS)
7
11
8
12
9 def get(section, name):
13 def get(section, name):
10 return config[section][name]
14 return config[section][name]
11
15
12
16
13 def get_int(section, name):
17 def get_int(section, name):
14 return int(get(section, name))
18 return int(get(section, name))
15
19
16
20
17 def get_bool(section, name):
21 def get_bool(section, name):
18 return get(section, name) == 'true'
22 return get(section, name) == 'true'
19
23
20
24
21 def get_list_dict(section, name):
25 def get_list_dict(section, name):
22 str_dict = get(section, name)
26 str_dict = get(section, name)
23 return [item.split(':') for item in str_dict.split(',')]
27 return [item.split(':') for item in str_dict.split(',')]
24
28
29
@@ -1,11 +1,12 b''
1 python-magic
1 python-magic
2 httplib2
2 httplib2
3 simplejson
3 simplejson
4 pytube
4 pytube
5 requests
5 requests
6 pillow
6 pillow
7 django>=1.8
7 django>=1.8
8 bbcode
8 bbcode
9 django-debug-toolbar
9 django-debug-toolbar
10 pytz
10 pytz
11 ecdsa
11 ecdsa
12 feedparser
General Comments 0
You need to be logged in to leave comments. Login now