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