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