| 1 |
from datetime import datetime |
|---|
| 2 |
|
|---|
| 3 |
from django.db import models |
|---|
| 4 |
from django.contrib.auth.models import User |
|---|
| 5 |
from django.core.urlresolvers import reverse |
|---|
| 6 |
from django.utils.html import escape, strip_tags |
|---|
| 7 |
from django.conf import settings |
|---|
| 8 |
from django.utils.translation import ugettext_lazy as _ |
|---|
| 9 |
|
|---|
| 10 |
from markdown import Markdown |
|---|
| 11 |
|
|---|
| 12 |
from pybb.markups import mypostmarkup |
|---|
| 13 |
from pybb.fields import AutoOneToOneField, ExtendedImageField |
|---|
| 14 |
from pybb.subscription import notify_subscribers |
|---|
| 15 |
from pybb.util import urlize |
|---|
| 16 |
|
|---|
| 17 |
LANGUAGE_CHOICES = ( |
|---|
| 18 |
('en', 'English'), |
|---|
| 19 |
('ru', _('Russian')), |
|---|
| 20 |
) |
|---|
| 21 |
|
|---|
| 22 |
TZ_CHOICES = [(float(x[0]), x[1]) for x in ( |
|---|
| 23 |
(-12, '-12'), (-11, '-11'), (-10, '-10'), (-9.5, '-09.5'), (-9, '-09'), |
|---|
| 24 |
(-8.5, '-08.5'), (-8, '-08 PST'), (-7, '-07 MST'), (-6, '-06 CST'), |
|---|
| 25 |
(-5, '-05 EST'), (-4, '-04 AST'), (-3.5, '-03.5'), (-3, '-03 ADT'), |
|---|
| 26 |
(-2, '-02'), (-1, '-01'), (0, '00 GMT'), (1, '+01 CET'), (2, '+02'), |
|---|
| 27 |
(3, '+03'), (3.5, '+03.5'), (4, '+04'), (4.5, '+04.5'), (5, '+05'), |
|---|
| 28 |
(5.5, '+05.5'), (6, '+06'), (6.5, '+06.5'), (7, '+07'), (8, '+08'), |
|---|
| 29 |
(9, '+09'), (9.5, '+09.5'), (10, '+10'), (10.5, '+10.5'), (11, '+11'), |
|---|
| 30 |
(11.5, '+11.5'), (12, '+12'), (13, '+13'), (14, '+14'), |
|---|
| 31 |
)] |
|---|
| 32 |
|
|---|
| 33 |
MARKUP_CHOICES = ( |
|---|
| 34 |
('bbcode', 'bbcode'), |
|---|
| 35 |
('markdown', 'markdown'), |
|---|
| 36 |
) |
|---|
| 37 |
|
|---|
| 38 |
|
|---|
| 39 |
class Category(models.Model): |
|---|
| 40 |
name = models.CharField(_('Name'), max_length=80) |
|---|
| 41 |
position = models.IntegerField(_('Position'), blank=True, default=0) |
|---|
| 42 |
|
|---|
| 43 |
class Meta: |
|---|
| 44 |
ordering = ['position'] |
|---|
| 45 |
verbose_name = _('Category') |
|---|
| 46 |
verbose_name_plural = _('Categories') |
|---|
| 47 |
|
|---|
| 48 |
def __unicode__(self): |
|---|
| 49 |
return self.name |
|---|
| 50 |
|
|---|
| 51 |
def forum_count(self): |
|---|
| 52 |
return self.forums.all().count() |
|---|
| 53 |
|
|---|
| 54 |
def get_absolute_url(self): |
|---|
| 55 |
return reverse('category', args=[self.id]) |
|---|
| 56 |
|
|---|
| 57 |
@property |
|---|
| 58 |
def topics(self): |
|---|
| 59 |
return Topic.objects.filter(forum__category=self).select_related() |
|---|
| 60 |
|
|---|
| 61 |
@property |
|---|
| 62 |
def posts(self): |
|---|
| 63 |
return Post.objects.filter(topic__forum__category=self).select_related() |
|---|
| 64 |
|
|---|
| 65 |
|
|---|
| 66 |
class Forum(models.Model): |
|---|
| 67 |
category = models.ForeignKey(Category, related_name='forums', verbose_name=_('Category')) |
|---|
| 68 |
name = models.CharField(_('Name'), max_length=80) |
|---|
| 69 |
position = models.IntegerField(_('Position'), blank=True, default=0) |
|---|
| 70 |
description = models.TextField(_('Description'), blank=True, default='') |
|---|
| 71 |
moderators = models.ManyToManyField(User, blank=True, null=True, verbose_name=_('Moderators')) |
|---|
| 72 |
updated = models.DateTimeField(_('Updated'), null=True) |
|---|
| 73 |
post_count = models.IntegerField(_('Post count'), blank=True, default=0) |
|---|
| 74 |
|
|---|
| 75 |
class Meta: |
|---|
| 76 |
ordering = ['position'] |
|---|
| 77 |
verbose_name = _('Forum') |
|---|
| 78 |
verbose_name_plural = _('Forums') |
|---|
| 79 |
|
|---|
| 80 |
def __unicode__(self): |
|---|
| 81 |
return self.name |
|---|
| 82 |
|
|---|
| 83 |
def topic_count(self): |
|---|
| 84 |
return self.topics.all().count() |
|---|
| 85 |
|
|---|
| 86 |
def get_absolute_url(self): |
|---|
| 87 |
return reverse('forum', args=[self.id]) |
|---|
| 88 |
|
|---|
| 89 |
@property |
|---|
| 90 |
def posts(self): |
|---|
| 91 |
return Post.objects.filter(topic__forum=self).select_related() |
|---|
| 92 |
|
|---|
| 93 |
@property |
|---|
| 94 |
def last_post(self): |
|---|
| 95 |
posts = self.posts.order_by('-created').select_related() |
|---|
| 96 |
try: |
|---|
| 97 |
return posts[0] |
|---|
| 98 |
except IndexError: |
|---|
| 99 |
return None |
|---|
| 100 |
|
|---|
| 101 |
|
|---|
| 102 |
class Topic(models.Model): |
|---|
| 103 |
forum = models.ForeignKey(Forum, related_name='topics', verbose_name=_('Forum')) |
|---|
| 104 |
name = models.CharField(_('Subject'), max_length=255) |
|---|
| 105 |
created = models.DateTimeField(_('Created'), null=True) |
|---|
| 106 |
updated = models.DateTimeField(_('Updated'), null=True) |
|---|
| 107 |
user = models.ForeignKey(User, verbose_name=_('User')) |
|---|
| 108 |
views = models.IntegerField(_('Views count'), blank=True, default=0) |
|---|
| 109 |
sticky = models.BooleanField(_('Sticky'), blank=True, default=False) |
|---|
| 110 |
closed = models.BooleanField(_('Closed'), blank=True, default=False) |
|---|
| 111 |
subscribers = models.ManyToManyField(User, related_name='subscriptions', verbose_name=_('Subscribers')) |
|---|
| 112 |
post_count = models.IntegerField(_('Post count'), blank=True, default=0) |
|---|
| 113 |
|
|---|
| 114 |
class Meta: |
|---|
| 115 |
ordering = ['-created'] |
|---|
| 116 |
verbose_name = _('Topic') |
|---|
| 117 |
verbose_name_plural = _('Topics') |
|---|
| 118 |
|
|---|
| 119 |
def __unicode__(self): |
|---|
| 120 |
return self.name |
|---|
| 121 |
|
|---|
| 122 |
@property |
|---|
| 123 |
def head(self): |
|---|
| 124 |
return self.posts.all().order_by('created').select_related()[0] |
|---|
| 125 |
|
|---|
| 126 |
@property |
|---|
| 127 |
def last_post(self): |
|---|
| 128 |
return self.posts.all().order_by('-created').select_related()[0] |
|---|
| 129 |
|
|---|
| 130 |
def get_absolute_url(self): |
|---|
| 131 |
return reverse('topic', args=[self.id]) |
|---|
| 132 |
|
|---|
| 133 |
def save(self, *args, **kwargs): |
|---|
| 134 |
if self.id is None: |
|---|
| 135 |
self.created = datetime.now() |
|---|
| 136 |
super(Topic, self).save(*args, **kwargs) |
|---|
| 137 |
|
|---|
| 138 |
def update_read(self, user): |
|---|
| 139 |
read, new = Read.objects.get_or_create(user=user, topic=self) |
|---|
| 140 |
if not new: |
|---|
| 141 |
read.time = datetime.now() |
|---|
| 142 |
read.save() |
|---|
| 143 |
|
|---|
| 144 |
|
|---|
| 145 |
|
|---|
| 146 |
|
|---|
| 147 |
|
|---|
| 148 |
|
|---|
| 149 |
|
|---|
| 150 |
|
|---|
| 151 |
|
|---|
| 152 |
|
|---|
| 153 |
class RenderableItem(models.Model): |
|---|
| 154 |
""" |
|---|
| 155 |
Base class for models that has markup, body, body_text and body_html fields. |
|---|
| 156 |
""" |
|---|
| 157 |
|
|---|
| 158 |
class Meta: |
|---|
| 159 |
abstract = True |
|---|
| 160 |
|
|---|
| 161 |
def render(self): |
|---|
| 162 |
if self.markup == 'bbcode': |
|---|
| 163 |
self.body_html = mypostmarkup.markup(self.body, auto_urls=False) |
|---|
| 164 |
elif self.markup == 'markdown': |
|---|
| 165 |
self.body_html = unicode(Markdown(self.body, safe_mode='escape')) |
|---|
| 166 |
else: |
|---|
| 167 |
raise Exception('Invalid markup property: %s' % self.markup) |
|---|
| 168 |
self.body_text = strip_tags(self.body_html) |
|---|
| 169 |
self.body_html = urlize(self.body_html) |
|---|
| 170 |
|
|---|
| 171 |
|
|---|
| 172 |
class Post(RenderableItem): |
|---|
| 173 |
topic = models.ForeignKey(Topic, related_name='posts', verbose_name=_('Topic')) |
|---|
| 174 |
user = models.ForeignKey(User, related_name='posts', verbose_name=_('User')) |
|---|
| 175 |
created = models.DateTimeField(_('Created'), blank=True) |
|---|
| 176 |
updated = models.DateTimeField(_('Updated'), blank=True, null=True) |
|---|
| 177 |
markup = models.CharField(_('Markup'), max_length=15, default=settings.PYBB_DEFAULT_MARKUP, choices=MARKUP_CHOICES) |
|---|
| 178 |
body = models.TextField(_('Message')) |
|---|
| 179 |
body_html = models.TextField(_('HTML version')) |
|---|
| 180 |
body_text = models.TextField(_('Text version')) |
|---|
| 181 |
user_ip = models.IPAddressField(_('User IP'), blank=True, default='') |
|---|
| 182 |
|
|---|
| 183 |
|
|---|
| 184 |
class Meta: |
|---|
| 185 |
ordering = ['created'] |
|---|
| 186 |
verbose_name = _('Post') |
|---|
| 187 |
verbose_name_plural = _('Posts') |
|---|
| 188 |
|
|---|
| 189 |
def summary(self): |
|---|
| 190 |
LIMIT = 50 |
|---|
| 191 |
tail = len(self.body) > LIMIT and '...' or '' |
|---|
| 192 |
return self.body[:LIMIT] + tail |
|---|
| 193 |
|
|---|
| 194 |
__unicode__ = summary |
|---|
| 195 |
|
|---|
| 196 |
def save(self, *args, **kwargs): |
|---|
| 197 |
if self.created is None: |
|---|
| 198 |
self.created = datetime.now() |
|---|
| 199 |
self.render() |
|---|
| 200 |
|
|---|
| 201 |
new = self.id is None |
|---|
| 202 |
|
|---|
| 203 |
if new: |
|---|
| 204 |
self.topic.updated = datetime.now() |
|---|
| 205 |
self.topic.post_count += 1 |
|---|
| 206 |
self.topic.save() |
|---|
| 207 |
self.topic.forum.updated = self.topic.updated |
|---|
| 208 |
self.topic.forum.post_count += 1 |
|---|
| 209 |
self.topic.forum.save() |
|---|
| 210 |
|
|---|
| 211 |
super(Post, self).save(*args, **kwargs) |
|---|
| 212 |
|
|---|
| 213 |
if new: |
|---|
| 214 |
notify_subscribers(self) |
|---|
| 215 |
|
|---|
| 216 |
|
|---|
| 217 |
|
|---|
| 218 |
|
|---|
| 219 |
def get_absolute_url(self): |
|---|
| 220 |
return reverse('post', args=[self.id]) |
|---|
| 221 |
|
|---|
| 222 |
def delete(self, *args, **kwargs): |
|---|
| 223 |
self_id = self.id |
|---|
| 224 |
head_post_id = self.topic.posts.order_by('created')[0].id |
|---|
| 225 |
super(Post, self).delete(*args, **kwargs) |
|---|
| 226 |
if self_id == head_post_id: |
|---|
| 227 |
self.topic.delete() |
|---|
| 228 |
|
|---|
| 229 |
|
|---|
| 230 |
class Profile(models.Model): |
|---|
| 231 |
user = AutoOneToOneField(User, related_name='pybb_profile', verbose_name=_('User')) |
|---|
| 232 |
site = models.URLField(_('Site'), verify_exists=False, blank=True, default='') |
|---|
| 233 |
jabber = models.CharField(_('Jabber'), max_length=80, blank=True, default='') |
|---|
| 234 |
icq = models.CharField(_('ICQ'), max_length=12, blank=True, default='') |
|---|
| 235 |
msn = models.CharField(_('MSN'), max_length=80, blank=True, default='') |
|---|
| 236 |
aim = models.CharField(_('AIM'), max_length=80, blank=True, default='') |
|---|
| 237 |
yahoo = models.CharField(_('Yahoo'), max_length=80, blank=True, default='') |
|---|
| 238 |
location = models.CharField(_('Location'), max_length=30, blank=True, default='') |
|---|
| 239 |
signature = models.TextField(_('Signature'), blank=True, default='', max_length=settings.PYBB_SIGNATURE_MAX_LENGTH) |
|---|
| 240 |
time_zone = models.FloatField(_('Time zone'), choices=TZ_CHOICES, default=float(settings.PYBB_DEFAULT_TIME_ZONE)) |
|---|
| 241 |
language = models.CharField(_('Language'), max_length=3, blank=True, default='en', choices=LANGUAGE_CHOICES) |
|---|
| 242 |
avatar = ExtendedImageField(_('Avatar'), blank=True, default='', upload_to=settings.PYBB_AVATARS_UPLOAD_TO, width=settings.PYBB_AVATAR_WIDTH, height=settings.PYBB_AVATAR_HEIGHT) |
|---|
| 243 |
show_signatures = models.BooleanField(_('Show signatures'), blank=True, default=True) |
|---|
| 244 |
markup = models.CharField(_('Default markup'), max_length=15, default=settings.PYBB_DEFAULT_MARKUP, choices=MARKUP_CHOICES) |
|---|
| 245 |
|
|---|
| 246 |
class Meta: |
|---|
| 247 |
verbose_name = _('Profile') |
|---|
| 248 |
verbose_name_plural = _('Profiles') |
|---|
| 249 |
|
|---|
| 250 |
|
|---|
| 251 |
class Read(models.Model): |
|---|
| 252 |
""" |
|---|
| 253 |
For each topic that user has entered the time |
|---|
| 254 |
is logged to this model. |
|---|
| 255 |
""" |
|---|
| 256 |
|
|---|
| 257 |
user = models.ForeignKey(User, verbose_name=_('User')) |
|---|
| 258 |
topic = models.ForeignKey(Topic, verbose_name=_('Topic')) |
|---|
| 259 |
time = models.DateTimeField(_('Time'), blank=True) |
|---|
| 260 |
|
|---|
| 261 |
class Meta: |
|---|
| 262 |
unique_together = ['user', 'topic'] |
|---|
| 263 |
verbose_name = _('Read') |
|---|
| 264 |
verbose_name_plural = _('Reads') |
|---|
| 265 |
|
|---|
| 266 |
def save(self, *args, **kwargs): |
|---|
| 267 |
if self.time is None: |
|---|
| 268 |
self.time = datetime.now() |
|---|
| 269 |
super(Read, self).save(*args, **kwargs) |
|---|
| 270 |
|
|---|
| 271 |
|
|---|
| 272 |
def __unicode__(self): |
|---|
| 273 |
return u'T[%d], U[%d]: %s' % (self.topic.id, self.user.id, unicode(self.time)) |
|---|
| 274 |
|
|---|
| 275 |
|
|---|
| 276 |
class PrivateMessage(RenderableItem): |
|---|
| 277 |
|
|---|
| 278 |
dst_user = models.ForeignKey(User, verbose_name=_('Recipient'), related_name='dst_users') |
|---|
| 279 |
src_user = models.ForeignKey(User, verbose_name=_('Author'), related_name='src_users') |
|---|
| 280 |
read = models.BooleanField(_('Read'), blank=True, default=False) |
|---|
| 281 |
created = models.DateTimeField(_('Created'), blank=True) |
|---|
| 282 |
markup = models.CharField(_('Markup'), max_length=15, default=settings.PYBB_DEFAULT_MARKUP, choices=MARKUP_CHOICES) |
|---|
| 283 |
subject = models.CharField(_('Subject'), max_length=255) |
|---|
| 284 |
body = models.TextField(_('Message')) |
|---|
| 285 |
body_html = models.TextField(_('HTML version')) |
|---|
| 286 |
body_text = models.TextField(_('Text version')) |
|---|
| 287 |
|
|---|
| 288 |
class Meta: |
|---|
| 289 |
ordering = ['-created'] |
|---|
| 290 |
verbose_name = _('Private message') |
|---|
| 291 |
verbose_name_plural = _('Private messages') |
|---|
| 292 |
|
|---|
| 293 |
|
|---|
| 294 |
|
|---|
| 295 |
def summary(self): |
|---|
| 296 |
LIMIT = 50 |
|---|
| 297 |
tail = len(self.body) > LIMIT and '...' or '' |
|---|
| 298 |
return self.body[:LIMIT] + tail |
|---|
| 299 |
|
|---|
| 300 |
def __unicode__(self): |
|---|
| 301 |
return self.subject |
|---|
| 302 |
|
|---|
| 303 |
def save(self, *args, **kwargs): |
|---|
| 304 |
if self.created is None: |
|---|
| 305 |
self.created = datetime.now() |
|---|
| 306 |
self.render() |
|---|
| 307 |
|
|---|
| 308 |
new = self.id is None |
|---|
| 309 |
super(PrivateMessage, self).save(*args, **kwargs) |
|---|
| 310 |
|
|---|
| 311 |
|
|---|
| 312 |
|
|---|
| 313 |
|
|---|
| 314 |
|
|---|