##// END OF EJS Templates
Optimized some post count calls.
neko259 -
r58:e922d63e default
parent child Browse files
Show More
@@ -1,265 +1,267 b''
1 1 import os
2 2 from random import random
3 3 import re
4 4 from django.db import models
5 5 from django.utils import timezone
6 6 import time
7 7 import math
8 8 import markdown
9 9 from markdown.inlinepatterns import Pattern
10 10 from markdown.util import etree
11 11
12 12 from neboard import settings
13 13 from markupfield.fields import MarkupField
14 14
15 15 import thumbs
16 16
17 17 NO_PARENT = -1
18 18 NO_IP = '0.0.0.0'
19 19 UNKNOWN_UA = ''
20 20
21 21
22 22 class PostManager(models.Manager):
23 23 ALL_PAGES = -1
24 24
25 25 def create_post(self, title, text, image=None, parent_id=NO_PARENT,
26 26 ip=NO_IP, tags=None):
27 27 post = self.create(title=title,
28 28 text=text,
29 29 pub_time=timezone.now(),
30 30 parent=parent_id,
31 31 image=image,
32 32 poster_ip=ip,
33 33 poster_user_agent=UNKNOWN_UA,
34 34 last_edit_time=timezone.now())
35 35
36 36 if tags:
37 37 for tag in tags:
38 38 post.tags.add(tag)
39 39
40 40 if parent_id != NO_PARENT:
41 41 self._bump_thread(parent_id)
42 42 else:
43 43 self._delete_old_threads()
44 44
45 45 return post
46 46
47 47 def delete_post(self, post):
48 48 children = self.filter(parent=post.id)
49 49 for child in children:
50 50 self.delete_post(child)
51 51 post.delete()
52 52
53 53 def delete_posts_by_ip(self, ip):
54 54 posts = self.filter(poster_ip=ip)
55 55 for post in posts:
56 56 self.delete_post(post)
57 57
58 58 def get_threads(self, tag=None, page=ALL_PAGES):
59 59 if tag:
60 60 threads = self.filter(parent=NO_PARENT, tags=tag)
61 61 else:
62 62 threads = self.filter(parent=NO_PARENT)
63 threads = list(threads.order_by('-last_edit_time'))
63 threads = threads.order_by('-last_edit_time')
64 64
65 65 if page != self.ALL_PAGES:
66 66 thread_count = len(threads)
67 67
68 68 if page < self.get_thread_page_count(tag=tag):
69 69 start_thread = page * settings.THREADS_PER_PAGE
70 70 end_thread = min(start_thread + settings.THREADS_PER_PAGE,
71 71 thread_count)
72 72 threads = threads[start_thread:end_thread]
73 73
74 74 return threads
75 75
76 76 def get_thread(self, opening_post_id):
77 77 opening_post = self.get(id=opening_post_id)
78 78
79 79 if opening_post.parent == NO_PARENT:
80 80 replies = self.filter(parent=opening_post_id)
81 81
82 82 thread = [opening_post]
83 83 thread.extend(replies)
84 84
85 85 return thread
86 86
87 87 def exists(self, post_id):
88 88 posts = self.filter(id=post_id)
89 89
90 return len(posts) > 0
90 return posts.count() > 0
91 91
92 92 def get_thread_page_count(self, tag=None):
93 93 if tag:
94 94 threads = self.filter(parent=NO_PARENT, tags=tag)
95 95 else:
96 96 threads = self.filter(parent=NO_PARENT)
97 97
98 return int(math.ceil(len(threads) / float(settings.THREADS_PER_PAGE)))
98 return int(math.ceil(threads.count() / float(
99 settings.THREADS_PER_PAGE)))
99 100
100 101 def _delete_old_threads(self):
101 102 """
102 103 Preserves maximum thread count. If there are too many threads,
103 104 delete the old ones.
104 105 """
105 106
106 # TODO Try to find a better way to get the active thread count.
107
108 107 # TODO Move old threads to the archive instead of deleting them.
109 108 # Maybe make some 'old' field in the model to indicate the thread
110 109 # must not be shown and be able for replying.
111 110
112 111 threads = self.get_threads()
113 112 thread_count = len(threads)
114 113
115 114 if thread_count > settings.MAX_THREAD_COUNT:
116 115 num_threads_to_delete = thread_count - settings.MAX_THREAD_COUNT
117 old_threads = threads[-num_threads_to_delete:]
116 old_threads = threads[thread_count - num_threads_to_delete:]
118 117
119 118 for thread in old_threads:
120 119 self.delete_post(thread)
121 120
122 121 def _bump_thread(self, thread_id):
123 122 thread = self.get(id=thread_id)
124 123
125 124 if thread.can_bump():
126 125 thread.last_edit_time = timezone.now()
127 126 thread.save()
128 127
129 128
130 129 class TagManager(models.Manager):
131 130 def get_not_empty_tags(self):
132 131 all_tags = self.all().order_by('name')
133 132 tags = []
134 133 for tag in all_tags:
135 134 if not tag.is_empty():
136 135 tags.append(tag)
137 136
138 137 return tags
139 138
140 139 def get_popular_tags(self):
141 140 all_tags = self.get_not_empty_tags()
142 141
143 142 sorted_tags = sorted(all_tags, key=lambda tag: tag.get_popularity(),
144 143 reverse=True)
145 144
146 145 return sorted_tags[:settings.POPULAR_TAGS]
147 146
148 147
149 148 class Tag(models.Model):
150 149 """
151 150 A tag is a text node assigned to the post. The tag serves as a board
152 151 section. There can be multiple tags for each message
153 152 """
154 153
154 OPENING_POST_WEIGHT = 5
155
155 156 objects = TagManager()
156 157
157 158 name = models.CharField(max_length=100)
158 159 # TODO Connect the tag to its posts to check the number of threads for
159 160 # the tag.
160 161
161 162 def __unicode__(self):
162 163 return self.name
163 164
164 165 def is_empty(self):
165 166 return self.get_post_count() == 0
166 167
167 168 def get_post_count(self):
168 169 posts_with_tag = Post.objects.get_threads(tag=self)
169 return len(posts_with_tag)
170 return posts_with_tag.count()
170 171
171 172 def get_popularity(self):
172 173 posts_with_tag = Post.objects.get_threads(tag=self)
173 174 reply_count = 0
174 175 for post in posts_with_tag:
175 176 reply_count += post.get_reply_count()
177 reply_count += self.OPENING_POST_WEIGHT
176 178
177 179 return reply_count
178 180
179 181
180 182 class Post(models.Model):
181 183 """A post is a message."""
182 184
183 185 IMAGES_DIRECTORY = 'images/'
184 186 FILE_EXTENSION_DELIMITER = '.'
185 187
186 188 objects = PostManager()
187 189
188 190 def _update_image_filename(self, filename):
189 191 """Get unique image filename"""
190 192
191 193 path = self.IMAGES_DIRECTORY
192 194 new_name = str(int(time.mktime(time.gmtime())))
193 195 new_name += str(int(random() * 1000))
194 196 new_name += self.FILE_EXTENSION_DELIMITER
195 197 new_name += filename.split(self.FILE_EXTENSION_DELIMITER)[-1:][0]
196 198
197 199 return os.path.join(path, new_name)
198 200
199 201 title = models.CharField(max_length=50)
200 202 pub_time = models.DateTimeField()
201 203 text = MarkupField(default_markup_type='markdown', escape_html=True)
202 204 image = thumbs.ImageWithThumbsField(upload_to=_update_image_filename,
203 205 blank=True, sizes=((200, 150),))
204 206 poster_ip = models.IPAddressField()
205 207 poster_user_agent = models.TextField()
206 208 parent = models.BigIntegerField()
207 209 tags = models.ManyToManyField(Tag)
208 210 last_edit_time = models.DateTimeField()
209 211
210 212 regex_pretty = re.compile(r'^\d(0)+$')
211 213 regex_same = re.compile(r'^(.)\1+$')
212 214
213 215 def __unicode__(self):
214 216 return self.title + ' (' + self.text.raw + ')'
215 217
216 218 def _get_replies(self):
217 219 return Post.objects.filter(parent=self.id)
218 220
219 221 def get_reply_count(self):
220 return len(self._get_replies())
222 return self._get_replies().count()
221 223
222 224 def get_images_count(self):
223 225 images_count = 1 if self.image else 0
224 226 for reply in self._get_replies():
225 227 if reply.image:
226 228 images_count += 1
227 229
228 230 return images_count
229 231
230 232 def get_gets_count(self):
231 233 gets_count = 1 if self.is_get() else 0
232 234 for reply in self._get_replies():
233 235 if reply.is_get():
234 236 gets_count += 1
235 237
236 238 return gets_count
237 239
238 240 def is_get(self):
239 241 """If the post has pretty id (1, 1000, 77777), than it is called GET"""
240 242
241 243 first = self.id == 1
242 244
243 245 id_str = str(self.id)
244 246 pretty = self.regex_pretty.match(id_str)
245 247 same_digits = self.regex_same.match(id_str)
246 248
247 249 return first or pretty or same_digits
248 250
249 251 def can_bump(self):
250 252 """Check if the thread can be bumped by replying"""
251 253
252 254 replies_count = len(Post.objects.get_thread(self.id))
253 255
254 256 return replies_count <= settings.MAX_POSTS_PER_THREAD
255 257
256 258
257 259 class Admin(models.Model):
258 260 """
259 261 Model for admin users
260 262 """
261 263 name = models.CharField(max_length=100)
262 264 password = models.CharField(max_length=100)
263 265
264 266 def __unicode__(self):
265 267 return self.name + '/' + '*' * len(self.password) No newline at end of file
@@ -1,186 +1,186 b''
1 1 # Django settings for neboard project.
2 2 import os
3 3 import markdown
4 4 from boards.mdx_neboard import markdown_extended
5 5
6 6 DEBUG = True
7 7 TEMPLATE_DEBUG = DEBUG
8 8
9 9 ADMINS = (
10 10 # ('Your Name', 'your_email@example.com'),
11 11 ('admin', 'admin@example.com')
12 12 )
13 13
14 14 MANAGERS = ADMINS
15 15
16 16 DATABASES = {
17 17 'default': {
18 18 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
19 19 'NAME': 'database.db', # Or path to database file if using sqlite3.
20 20 'USER': '', # Not used with sqlite3.
21 21 'PASSWORD': '', # Not used with sqlite3.
22 22 'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
23 23 'PORT': '', # Set to empty string for default. Not used with sqlite3.
24 24 }
25 25 }
26 26
27 27 # Local time zone for this installation. Choices can be found here:
28 28 # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
29 29 # although not all choices may be available on all operating systems.
30 30 # In a Windows environment this must be set to your system time zone.
31 31 TIME_ZONE = 'Europe/Kiev'
32 32
33 33 # Language code for this installation. All choices can be found here:
34 34 # http://www.i18nguy.com/unicode/language-identifiers.html
35 35 LANGUAGE_CODE = 'ru'
36 36
37 37 SITE_ID = 1
38 38
39 39 # If you set this to False, Django will make some optimizations so as not
40 40 # to load the internationalization machinery.
41 41 USE_I18N = True
42 42
43 43 # If you set this to False, Django will not format dates, numbers and
44 44 # calendars according to the current locale.
45 45 USE_L10N = True
46 46
47 47 # If you set this to False, Django will not use timezone-aware datetimes.
48 48 USE_TZ = True
49 49
50 50 # Absolute filesystem path to the directory that will hold user-uploaded files.
51 51 # Example: "/home/media/media.lawrence.com/media/"
52 52 MEDIA_ROOT = './media/'
53 53
54 54 # URL that handles the media served from MEDIA_ROOT. Make sure to use a
55 55 # trailing slash.
56 56 # Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
57 57 MEDIA_URL = '/media/'
58 58
59 59 # Absolute path to the directory static files should be collected to.
60 60 # Don't put anything in this directory yourself; store your static files
61 61 # in apps' "static/" subdirectories and in STATICFILES_DIRS.
62 62 # Example: "/home/media/media.lawrence.com/static/"
63 63 STATIC_ROOT = ''
64 64
65 65 # URL prefix for static files.
66 66 # Example: "http://media.lawrence.com/static/"
67 67 STATIC_URL = '/static/'
68 68
69 69 # Additional locations of static files
70 70 # It is really a hack, put real paths, not related
71 71 STATICFILES_DIRS = (
72 72 os.path.dirname(__file__) + '/boards/static',
73 73
74 74 # '/d/work/python/django/neboard/neboard/boards/static',
75 75 # Put strings here, like "/home/html/static" or "C:/www/django/static".
76 76 # Always use forward slashes, even on Windows.
77 77 # Don't forget to use absolute paths, not relative paths.
78 78 )
79 79
80 80 # List of finder classes that know how to find static files in
81 81 # various locations.
82 82 STATICFILES_FINDERS = (
83 83 'django.contrib.staticfiles.finders.FileSystemFinder',
84 84 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
85 85 # 'django.contrib.staticfiles.finders.DefaultStorageFinder',
86 86 )
87 87
88 88 # Make this unique, and don't share it with anybody.
89 89 SECRET_KEY = '@1rc$o(7=tt#kd+4s$u6wchm**z^)4x90)7f6z(i&amp;55@o11*8o'
90 90
91 91 # List of callables that know how to import templates from various sources.
92 92 TEMPLATE_LOADERS = (
93 93 'django.template.loaders.filesystem.Loader',
94 94 'django.template.loaders.app_directories.Loader',
95 95 # 'django.template.loaders.eggs.Loader',
96 96 )
97 97
98 98 TEMPLATE_CONTEXT_PROCESSORS = (
99 99 'django.core.context_processors.media',
100 100 'django.core.context_processors.static',
101 101 'django.core.context_processors.request',
102 102 'django.contrib.auth.context_processors.auth',
103 103 )
104 104
105 105 MIDDLEWARE_CLASSES = (
106 106 'django.middleware.common.CommonMiddleware',
107 107 'django.contrib.sessions.middleware.SessionMiddleware',
108 108 # 'django.middleware.csrf.CsrfViewMiddleware',
109 109 'django.contrib.auth.middleware.AuthenticationMiddleware',
110 110 'django.contrib.messages.middleware.MessageMiddleware',
111 111 # Uncomment the next line for simple clickjacking protection:
112 112 # 'django.middleware.clickjacking.XFrameOptionsMiddleware',
113 113 )
114 114
115 115 ROOT_URLCONF = 'neboard.urls'
116 116
117 117 # Python dotted path to the WSGI application used by Django's runserver.
118 118 WSGI_APPLICATION = 'neboard.wsgi.application'
119 119
120 120 TEMPLATE_DIRS = (
121 121 # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
122 122 # Always use forward slashes, even on Windows.
123 123 # Don't forget to use absolute paths, not relative paths.
124 124 'templates',
125 125 )
126 126
127 127 INSTALLED_APPS = (
128 128 'django.contrib.auth',
129 129 'django.contrib.contenttypes',
130 130 'django.contrib.sessions',
131 131 'django.contrib.sites',
132 132 'django.contrib.messages',
133 133 'django.contrib.staticfiles',
134 134 # Uncomment the next line to enable the admin:
135 135 'django.contrib.admin',
136 136 # Uncomment the next line to enable admin documentation:
137 137 # 'django.contrib.admindocs',
138 138 'django.contrib.markup',
139 139 'django_cleanup',
140 140 'boards',
141 141 )
142 142
143 143 # A sample logging configuration. The only tangible logging
144 144 # performed by this configuration is to send an email to
145 145 # the site admins on every HTTP 500 error when DEBUG=False.
146 146 # See http://docs.djangoproject.com/en/dev/topics/logging for
147 147 # more details on how to customize your logging configuration.
148 148 LOGGING = {
149 149 'version': 1,
150 150 'disable_existing_loggers': False,
151 151 'filters': {
152 152 'require_debug_false': {
153 153 '()': 'django.utils.log.RequireDebugFalse'
154 154 }
155 155 },
156 156 'handlers': {
157 157 'mail_admins': {
158 158 'level': 'ERROR',
159 159 'filters': ['require_debug_false'],
160 160 'class': 'django.utils.log.AdminEmailHandler'
161 161 }
162 162 },
163 163 'loggers': {
164 164 'django.request': {
165 165 'handlers': ['mail_admins'],
166 166 'level': 'ERROR',
167 167 'propagate': True,
168 168 },
169 169 }
170 170 }
171 171
172 172 MARKUP_FIELD_TYPES = (
173 173 ('markdown', markdown_extended),
174 174 )
175 175 # Custom imageboard settings
176 176 MAX_POSTS_PER_THREAD = 10 # Thread bumplimit
177 177 MAX_THREAD_COUNT = 500 # Old threads will be deleted to preserve this count
178 178 THREADS_PER_PAGE = 10
179 179 SITE_NAME = 'Neboard'
180 180
181 181 THEMES = [
182 182 ('md', 'Mystic Dark'),
183 183 ('sw', 'Snow White') ]
184 184 DEFAULT_THEME = 'md'
185 185
186 POPULAR_TAGS=10 No newline at end of file
186 POPULAR_TAGS = 10 No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now