2012-08-10 11:12:52 +03:00
# coding: utf-8
2012-04-23 14:28:40 +03:00
from hashlib import sha1
2012-08-10 11:12:52 +03:00
import os
2009-04-14 14:57:17 +03:00
2009-01-05 14:30:08 +02:00
from django . conf import settings
2012-08-10 11:12:52 +03:00
from django . contrib . auth . models import User , Group
2015-01-08 14:30:18 -05:00
from django . core . urlresolvers import reverse
from django . core . validators import MaxLengthValidator
2012-08-10 11:12:52 +03:00
from django . db import models
from django . db . models import aggregates
2011-08-18 10:42:44 +03:00
from django . db . models . signals import post_save
2012-11-13 17:57:39 -05:00
from django . shortcuts import get_object_or_404
2012-08-10 11:12:52 +03:00
from django . utils . translation import ugettext_lazy as _
2013-01-30 19:15:52 -05:00
from django . utils import timezone
2009-01-05 14:30:08 +02:00
2014-03-12 20:18:44 -04:00
from django_fsm . db . fields import FSMField , transition , can_proceed
2014-02-26 17:54:17 -05:00
2009-12-23 17:06:48 +02:00
from djangobb_forum . fields import AutoOneToOneField , ExtendedImageField , JSONField
2010-11-28 17:57:54 +02:00
from djangobb_forum . util import smiles , convert_text_to_html
2009-12-23 17:06:48 +02:00
from djangobb_forum import settings as forum_settings
2009-01-05 14:30:08 +02:00
2010-11-06 19:54:22 +02:00
if ' south ' in settings . INSTALLED_APPS :
from south . modelsinspector import add_introspection_rules
add_introspection_rules ( [ ] , [ ' ^djangobb_forum \ .fields \ .AutoOneToOneField ' ,
' ^djangobb_forum \ .fields \ .JSONField ' ,
' ^djangobb_forum \ .fields \ .ExtendedImageField ' ] )
2009-01-05 14:30:08 +02:00
TZ_CHOICES = [ ( float ( x [ 0 ] ) , x [ 1 ] ) for x in (
( - 12 , ' -12 ' ) , ( - 11 , ' -11 ' ) , ( - 10 , ' -10 ' ) , ( - 9.5 , ' -09.5 ' ) , ( - 9 , ' -09 ' ) ,
( - 8.5 , ' -08.5 ' ) , ( - 8 , ' -08 PST ' ) , ( - 7 , ' -07 MST ' ) , ( - 6 , ' -06 CST ' ) ,
( - 5 , ' -05 EST ' ) , ( - 4 , ' -04 AST ' ) , ( - 3.5 , ' -03.5 ' ) , ( - 3 , ' -03 ADT ' ) ,
( - 2 , ' -02 ' ) , ( - 1 , ' -01 ' ) , ( 0 , ' 00 GMT ' ) , ( 1 , ' +01 CET ' ) , ( 2 , ' +02 ' ) ,
( 3 , ' +03 ' ) , ( 3.5 , ' +03.5 ' ) , ( 4 , ' +04 ' ) , ( 4.5 , ' +04.5 ' ) , ( 5 , ' +05 ' ) ,
( 5.5 , ' +05.5 ' ) , ( 6 , ' +06 ' ) , ( 6.5 , ' +06.5 ' ) , ( 7 , ' +07 ' ) , ( 8 , ' +08 ' ) ,
( 9 , ' +09 ' ) , ( 9.5 , ' +09.5 ' ) , ( 10 , ' +10 ' ) , ( 10.5 , ' +10.5 ' ) , ( 11 , ' +11 ' ) ,
( 11.5 , ' +11.5 ' ) , ( 12 , ' +12 ' ) , ( 13 , ' +13 ' ) , ( 14 , ' +14 ' ) ,
) ]
SIGN_CHOICES = (
( 1 , ' PLUS ' ) ,
( - 1 , ' MINUS ' ) ,
)
PRIVACY_CHOICES = (
( 0 , _ ( u ' Display your e-mail address. ' ) ) ,
( 1 , _ ( u ' Hide your e-mail address but allow form e-mail. ' ) ) ,
( 2 , _ ( u ' Hide your e-mail address and disallow form e-mail. ' ) ) ,
)
2010-11-28 17:57:54 +02:00
MARKUP_CHOICES = [ ( ' bbcode ' , ' bbcode ' ) ]
try :
import markdown
MARKUP_CHOICES . append ( ( " markdown " , " markdown " ) )
except ImportError :
pass
2009-01-05 14:30:08 +02:00
2011-12-22 11:18:48 +02:00
path = os . path . join ( settings . STATIC_ROOT , ' djangobb_forum ' , ' themes ' )
2011-11-09 14:22:58 +02:00
if os . path . exists ( path ) :
# fix for collectstatic
2012-09-24 14:18:16 +03:00
THEME_CHOICES = [ ( theme , theme ) for theme in os . listdir ( path )
2011-11-09 14:22:58 +02:00
if os . path . isdir ( os . path . join ( path , theme ) ) ]
else :
THEME_CHOICES = [ ]
2009-01-05 14:30:08 +02:00
2014-02-26 17:54:17 -05:00
import logging
logger = logging . getLogger ( __name__ )
akismet_api = None
2014-12-23 11:53:39 -05:00
from akismet import Akismet , AkismetError
2014-02-26 17:54:17 -05:00
try :
2014-05-24 12:18:53 -04:00
if getattr ( settings , ' AKISMET_ENABLED ' , True ) :
2014-04-24 16:33:39 -04:00
akismet_api = Akismet ( key = forum_settings . AKISMET_API_KEY , blog_url = forum_settings . AKISMET_BLOG_URL , agent = forum_settings . AKISMET_AGENT )
2014-02-26 17:54:17 -05:00
except Exception as e :
logger . error ( " Error while initializing Akismet " , extra = { ' exception ' : e } )
2009-01-05 14:30:08 +02:00
class Category ( models . Model ) :
name = models . CharField ( _ ( ' Name ' ) , max_length = 80 )
2012-09-24 14:18:16 +03:00
groups = models . ManyToManyField ( Group , blank = True , null = True , verbose_name = _ ( ' Groups ' ) , help_text = _ ( ' Only users from these groups can see this category ' ) )
2009-01-05 14:30:08 +02:00
position = models . IntegerField ( _ ( ' Position ' ) , blank = True , default = 0 )
class Meta :
ordering = [ ' position ' ]
verbose_name = _ ( ' Category ' )
verbose_name_plural = _ ( ' Categories ' )
def __unicode__ ( self ) :
return self . name
def forum_count ( self ) :
return self . forums . all ( ) . count ( )
@property
def topics ( self ) :
2010-11-29 00:57:19 +02:00
return Topic . objects . filter ( forum__category__id = self . id ) . select_related ( )
2009-12-24 17:24:39 +02:00
2009-01-05 14:30:08 +02:00
@property
def posts ( self ) :
2010-11-29 00:57:19 +02:00
return Post . objects . filter ( topic__forum__category__id = self . id ) . select_related ( )
2009-01-05 14:30:08 +02:00
2009-04-07 17:46:22 +03:00
def has_access ( self , user ) :
2012-09-24 14:18:16 +03:00
if user . is_superuser :
return True
2011-01-13 17:36:29 +02:00
if self . groups . exists ( ) :
2012-09-24 14:18:16 +03:00
if user . is_authenticated ( ) :
2012-09-24 14:18:16 +03:00
if not self . groups . filter ( user__pk = user . id ) . exists ( ) :
return False
2009-07-27 13:05:19 +03:00
else :
2009-04-07 18:43:04 +03:00
return False
return True
2009-01-05 14:30:08 +02:00
2009-07-27 13:05:19 +03:00
2009-01-05 14:30:08 +02:00
class Forum ( models . Model ) :
category = models . ForeignKey ( Category , related_name = ' forums ' , verbose_name = _ ( ' Category ' ) )
2013-01-27 17:24:14 -05:00
moderator_only = models . BooleanField ( _ ( ' New topics by moderators only ' ) , default = False )
2009-01-05 14:30:08 +02:00
name = models . CharField ( _ ( ' Name ' ) , max_length = 80 )
position = models . IntegerField ( _ ( ' Position ' ) , blank = True , default = 0 )
description = models . TextField ( _ ( ' Description ' ) , blank = True , default = ' ' )
moderators = models . ManyToManyField ( User , blank = True , null = True , verbose_name = _ ( ' Moderators ' ) )
2009-12-24 17:24:39 +02:00
updated = models . DateTimeField ( _ ( ' Updated ' ) , auto_now = True )
2009-01-05 14:30:08 +02:00
post_count = models . IntegerField ( _ ( ' Post count ' ) , blank = True , default = 0 )
2009-05-24 18:50:57 +03:00
topic_count = models . IntegerField ( _ ( ' Topic count ' ) , blank = True , default = 0 )
2009-10-19 17:23:36 +03:00
last_post = models . ForeignKey ( ' Post ' , related_name = ' last_forum_post ' , blank = True , null = True )
2009-01-05 14:30:08 +02:00
class Meta :
ordering = [ ' position ' ]
verbose_name = _ ( ' Forum ' )
verbose_name_plural = _ ( ' Forums ' )
def __unicode__ ( self ) :
return self . name
2009-11-29 18:56:11 +02:00
@models.permalink
2009-01-05 14:30:08 +02:00
def get_absolute_url ( self ) :
2009-11-29 18:56:11 +02:00
return ( ' djangobb:forum ' , [ self . id ] )
2009-05-24 18:50:57 +03:00
2013-02-20 17:27:44 -05:00
def get_mobile_url ( self ) :
return reverse ( ' djangobb:mobile_forum ' , args = [ self . id ] )
2009-01-05 14:30:08 +02:00
@property
def posts ( self ) :
2010-11-29 00:57:19 +02:00
return Post . objects . filter ( topic__forum__id = self . id ) . select_related ( )
2009-01-05 14:30:08 +02:00
2014-03-04 17:24:30 -05:00
def set_last_post ( self ) :
try :
self . last_post = Topic . objects . filter ( forum = self ) . latest ( ) . last_post
except Topic . DoesNotExist :
self . last_post = None
def set_counts ( self ) :
self . topic_count = Topic . objects . filter ( forum = self ) . count ( )
self . post_count = Post . objects . filter ( topic__forum = self ) . count ( )
2009-01-05 14:30:08 +02:00
class Topic ( models . Model ) :
forum = models . ForeignKey ( Forum , related_name = ' topics ' , verbose_name = _ ( ' Forum ' ) )
name = models . CharField ( _ ( ' Subject ' ) , max_length = 255 )
2009-12-24 17:24:39 +02:00
created = models . DateTimeField ( _ ( ' Created ' ) , auto_now_add = True )
2009-12-29 16:32:14 +02:00
updated = models . DateTimeField ( _ ( ' Updated ' ) , null = True )
2009-01-05 14:30:08 +02:00
user = models . ForeignKey ( User , verbose_name = _ ( ' User ' ) )
views = models . IntegerField ( _ ( ' Views count ' ) , blank = True , default = 0 )
sticky = models . BooleanField ( _ ( ' Sticky ' ) , blank = True , default = False )
closed = models . BooleanField ( _ ( ' Closed ' ) , blank = True , default = False )
2009-01-14 17:31:10 +02:00
subscribers = models . ManyToManyField ( User , related_name = ' subscriptions ' , verbose_name = _ ( ' Subscribers ' ) , blank = True )
2009-01-05 14:30:08 +02:00
post_count = models . IntegerField ( _ ( ' Post count ' ) , blank = True , default = 0 )
2009-05-24 18:50:57 +03:00
last_post = models . ForeignKey ( ' Post ' , related_name = ' last_topic_post ' , blank = True , null = True )
2009-01-05 14:30:08 +02:00
class Meta :
2009-01-22 14:38:28 +02:00
ordering = [ ' -updated ' ]
2011-05-16 10:40:54 +03:00
get_latest_by = ' updated '
2009-01-05 14:30:08 +02:00
verbose_name = _ ( ' Topic ' )
verbose_name_plural = _ ( ' Topics ' )
2013-01-17 22:09:37 -05:00
permissions = (
2013-02-19 15:15:06 -05:00
( ' delayed_close ' , ' Can close topics after a delay ' ) ,
2013-01-17 22:09:37 -05:00
)
2009-01-05 14:30:08 +02:00
def __unicode__ ( self ) :
return self . name
2009-12-24 17:24:39 +02:00
2014-03-10 13:29:16 -04:00
def move_to ( self , new_forum ) :
"""
Move a topic to a new forum .
"""
self . clear_last_forum_post ( )
old_forum = self . forum
self . forum = new_forum
self . save ( )
old_forum . set_last_post ( )
old_forum . set_counts ( )
old_forum . save ( )
2011-05-16 10:40:54 +03:00
def delete ( self , * args , * * kwargs ) :
2014-03-10 13:29:16 -04:00
self . clear_last_forum_post ( )
2011-05-16 10:40:54 +03:00
forum = self . forum
2013-07-05 21:52:44 +00:00
if forum_settings . SOFT_DELETE_TOPICS and ( self . forum != get_object_or_404 ( Forum , pk = forum_settings . SOFT_DELETE_TOPICS ) or not kwargs . get ( ' staff ' , False ) ) :
2012-11-19 11:55:36 -05:00
self . forum = get_object_or_404 ( Forum , pk = forum_settings . SOFT_DELETE_TOPICS )
self . save ( )
else :
2013-07-05 22:03:51 +00:00
super ( Topic , self ) . delete ( )
2014-03-04 17:24:30 -05:00
forum . set_last_post ( )
forum . set_counts ( )
2011-05-16 10:40:54 +03:00
forum . save ( )
2009-01-05 14:30:08 +02:00
@property
def head ( self ) :
2009-07-03 15:29:31 +03:00
try :
return self . posts . select_related ( ) . order_by ( ' created ' ) [ 0 ]
except IndexError :
return None
2009-01-05 14:30:08 +02:00
@property
def reply_count ( self ) :
return self . post_count - 1
2009-11-29 18:56:11 +02:00
@models.permalink
2009-01-05 14:30:08 +02:00
def get_absolute_url ( self ) :
2009-11-29 18:56:11 +02:00
return ( ' djangobb:topic ' , [ self . id ] )
2009-01-05 14:30:08 +02:00
2013-02-20 17:27:44 -05:00
def get_mobile_url ( self ) :
return reverse ( ' djangobb:mobile_topic ' , args = [ self . id ] )
2009-01-05 14:30:08 +02:00
def update_read ( self , user ) :
2009-10-22 15:19:26 +03:00
tracking = user . posttracking
#if last_read > last_read - don't check topics
if tracking . last_read and ( tracking . last_read > self . last_post . created ) :
return
if isinstance ( tracking . topics , dict ) :
#clear topics if len > 5Kb and set last_read to current time
if len ( tracking . topics ) > 5120 :
tracking . topics = None
2013-01-30 19:15:52 -05:00
tracking . last_read = timezone . now ( )
2009-10-22 15:19:26 +03:00
tracking . save ( )
#update topics if exist new post or does't exist in dict
2014-02-05 09:45:49 -05:00
elif self . last_post_id > tracking . topics . get ( str ( self . id ) , 0 ) :
2012-02-16 23:56:56 +02:00
tracking . topics [ str ( self . id ) ] = self . last_post_id
2009-10-22 15:19:26 +03:00
tracking . save ( )
else :
#initialize topic tracking dict
2012-02-16 23:56:56 +02:00
tracking . topics = { self . id : self . last_post_id }
2009-10-22 15:19:26 +03:00
tracking . save ( )
2009-01-05 14:30:08 +02:00
2014-03-10 13:29:16 -04:00
def clear_last_forum_post ( self ) :
"""
Prep for moving / deleting . Update the forum the topic belongs to .
"""
try :
last_post = self . posts . latest ( )
last_post . last_forum_post . clear ( )
except Post . DoesNotExist :
pass
else :
last_post . last_forum_post . clear ( )
2009-01-05 14:30:08 +02:00
class Post ( models . Model ) :
topic = models . ForeignKey ( Topic , related_name = ' posts ' , verbose_name = _ ( ' Topic ' ) )
user = models . ForeignKey ( User , related_name = ' posts ' , verbose_name = _ ( ' User ' ) )
2009-12-24 17:24:39 +02:00
created = models . DateTimeField ( _ ( ' Created ' ) , auto_now_add = True )
2009-01-05 14:30:08 +02:00
updated = models . DateTimeField ( _ ( ' Updated ' ) , blank = True , null = True )
2010-02-17 13:05:48 +02:00
updated_by = models . ForeignKey ( User , verbose_name = _ ( ' Updated by ' ) , blank = True , null = True )
2009-01-17 17:56:19 +02:00
markup = models . CharField ( _ ( ' Markup ' ) , max_length = 15 , default = forum_settings . DEFAULT_MARKUP , choices = MARKUP_CHOICES )
2015-01-08 14:30:18 -05:00
body = models . TextField ( _ ( ' Message ' ) , validators = [ MaxLengthValidator ( forum_settings . POST_MAX_LENGTH ) ] )
2009-01-05 14:30:08 +02:00
body_html = models . TextField ( _ ( ' HTML version ' ) )
2009-11-16 14:50:39 +02:00
user_ip = models . IPAddressField ( _ ( ' User IP ' ) , blank = True , null = True )
2009-01-05 14:30:08 +02:00
class Meta :
ordering = [ ' created ' ]
2009-10-19 17:23:36 +03:00
get_latest_by = ' created '
2009-01-05 14:30:08 +02:00
verbose_name = _ ( ' Post ' )
verbose_name_plural = _ ( ' Posts ' )
2012-12-10 11:07:15 -05:00
permissions = (
( ' fast_post ' , ' Can add posts without a time limit ' ) ,
( ' med_post ' , ' Can add posts at medium speed ' ) ,
2013-01-17 20:28:40 -05:00
( ' post_external_links ' , ' Can post external links ' ) ,
2013-02-19 15:15:06 -05:00
( ' delayed_delete ' , ' Can delete posts after a delay ' ) ,
2014-12-19 16:42:18 -05:00
)
2009-01-05 14:30:08 +02:00
def save ( self , * args , * * kwargs ) :
2013-01-17 20:28:40 -05:00
self . body_html = convert_text_to_html ( self . body , self . user . forum_profile )
2010-11-08 19:15:48 +02:00
if forum_settings . SMILES_SUPPORT and self . user . forum_profile . show_smilies :
2009-05-25 14:08:24 +03:00
self . body_html = smiles ( self . body_html )
2009-05-24 18:50:57 +03:00
super ( Post , self ) . save ( * args , * * kwargs )
2009-01-05 14:30:08 +02:00
2014-03-18 13:41:47 -04:00
def move_to ( self , to_topic , delete_topic = True ) :
2014-03-18 13:54:23 -04:00
delete_topic = ( self . topic . posts . count ( ) == 1 ) and delete_topic
2014-03-10 13:29:16 -04:00
prev_topic = self . topic
self . topic = to_topic
self . save ( )
self . set_counts ( )
if delete_topic :
prev_topic . delete ( )
prev_topic . forum . set_last_post ( )
prev_topic . forum . set_counts ( )
prev_topic . forum . save ( )
2010-02-17 12:41:01 +02:00
2009-01-05 14:30:08 +02:00
def delete ( self , * args , * * kwargs ) :
self_id = self . id
2009-10-21 13:35:14 +03:00
head_post_id = self . topic . posts . order_by ( ' created ' ) [ 0 ] . id
forum = self . topic . forum
topic = self . topic
2010-02-17 12:41:01 +02:00
profile = self . user . forum_profile
2009-10-19 17:23:36 +03:00
self . last_topic_post . clear ( )
self . last_forum_post . clear ( )
2012-11-13 17:57:39 -05:00
# If we actually delete the post, we lose any reports that my have come from it. Also, there is no recovery (but I don't care about that as much right now)
2013-02-03 22:50:59 -05:00
if self_id == head_post_id :
2013-07-05 22:03:51 +00:00
topic . delete ( * args , * * kwargs )
2009-10-21 13:35:14 +03:00
else :
2013-07-05 21:52:44 +00:00
if forum_settings . SOFT_DELETE_POSTS and ( self . topic != get_object_or_404 ( Topic , pk = forum_settings . SOFT_DELETE_POSTS ) or not kwargs . get ( ' staff ' , False ) ) :
2013-02-03 22:50:59 -05:00
self . topic = get_object_or_404 ( Topic , pk = forum_settings . SOFT_DELETE_POSTS )
self . save ( )
else :
2013-07-05 22:03:51 +00:00
super ( Post , self ) . delete ( )
2013-02-03 22:50:59 -05:00
#if post was last in topic - remove topic
2013-02-03 23:00:40 -05:00
try :
topic . last_post = Post . objects . filter ( topic__id = topic . id ) . latest ( )
except Post . DoesNotExist :
topic . last_post = None
topic . post_count = Post . objects . filter ( topic__id = topic . id ) . count ( )
topic . save ( )
2014-03-04 17:24:30 -05:00
forum . set_last_post ( )
forum . save ( )
self . set_counts ( )
def set_counts ( self ) :
"""
Recounts this post ' s forum and and topic post counts.
"""
forum = self . topic . forum
profile = self . user . forum_profile
2010-02-17 13:05:48 +02:00
#TODO: for speedup - save/update only changed fields
2014-03-04 17:24:30 -05:00
forum . set_counts ( )
2009-10-21 13:35:14 +03:00
forum . save ( )
2014-03-04 17:24:30 -05:00
profile . set_counts ( )
2010-02-17 12:41:01 +02:00
profile . save ( )
2009-10-21 13:35:14 +03:00
2009-11-29 18:56:11 +02:00
@models.permalink
def get_absolute_url ( self ) :
return ( ' djangobb:post ' , [ self . id ] )
2013-02-20 17:27:44 -05:00
def get_mobile_url ( self ) :
return reverse ( ' djangobb:mobile_post ' , args = [ self . id ] )
2009-11-29 18:56:11 +02:00
def summary ( self ) :
LIMIT = 50
2011-11-09 14:22:58 +02:00
tail = len ( self . body ) > LIMIT and ' ... ' or ' '
2009-11-29 18:56:11 +02:00
return self . body [ : LIMIT ] + tail
__unicode__ = summary
2009-01-05 14:30:08 +02:00
class Reputation ( models . Model ) :
from_user = models . ForeignKey ( User , related_name = ' reputations_from ' , verbose_name = _ ( ' From ' ) )
to_user = models . ForeignKey ( User , related_name = ' reputations_to ' , verbose_name = _ ( ' To ' ) )
2010-02-23 14:19:20 +02:00
post = models . ForeignKey ( Post , related_name = ' post ' , verbose_name = _ ( ' Post ' ) )
time = models . DateTimeField ( _ ( ' Time ' ) , auto_now_add = True )
2009-01-05 14:30:08 +02:00
sign = models . IntegerField ( _ ( ' Sign ' ) , choices = SIGN_CHOICES , default = 0 )
2010-02-23 14:19:20 +02:00
reason = models . TextField ( _ ( ' Reason ' ) , max_length = 1000 )
2009-10-22 15:19:26 +03:00
2009-01-05 14:30:08 +02:00
class Meta :
verbose_name = _ ( ' Reputation ' )
verbose_name_plural = _ ( ' Reputations ' )
2010-02-23 14:19:20 +02:00
unique_together = ( ( ' from_user ' , ' post ' ) , )
2009-01-05 14:30:08 +02:00
def __unicode__ ( self ) :
2010-02-23 14:19:20 +02:00
return u ' T[ %d ], FU[ %d ], TU[ %d ]: %s ' % ( self . post . id , self . from_user . id , self . to_user . id , unicode ( self . time ) )
2009-07-02 16:59:07 +03:00
2012-03-03 23:24:39 +02:00
class ProfileManager ( models . Manager ) :
2012-03-04 13:35:02 +02:00
use_for_related_fields = True
2012-03-03 23:24:39 +02:00
def get_query_set ( self ) :
qs = super ( ProfileManager , self ) . get_query_set ( )
if forum_settings . REPUTATION_SUPPORT :
2012-03-04 13:35:02 +02:00
qs = qs . extra ( select = {
2012-03-04 22:10:32 +02:00
' reply_total ' : ' SELECT SUM(sign) FROM djangobb_forum_reputation WHERE to_user_id = djangobb_forum_profile.user_id GROUP BY to_user_id ' ,
' reply_count_minus ' : " SELECT SUM(sign) FROM djangobb_forum_reputation WHERE to_user_id = djangobb_forum_profile.user_id AND sign = ' -1 ' GROUP BY to_user_id " ,
' reply_count_plus ' : " SELECT SUM(sign) FROM djangobb_forum_reputation WHERE to_user_id = djangobb_forum_profile.user_id AND sign = ' 1 ' GROUP BY to_user_id " ,
2012-03-04 13:35:02 +02:00
} )
2012-03-03 23:24:39 +02:00
return qs
2009-01-05 14:30:08 +02:00
class Profile ( models . Model ) :
user = AutoOneToOneField ( User , related_name = ' forum_profile ' , verbose_name = _ ( ' User ' ) )
2009-11-15 00:01:26 +02:00
status = models . CharField ( _ ( ' Status ' ) , max_length = 30 , blank = True )
site = models . URLField ( _ ( ' Site ' ) , verify_exists = False , blank = True )
jabber = models . CharField ( _ ( ' Jabber ' ) , max_length = 80 , blank = True )
icq = models . CharField ( _ ( ' ICQ ' ) , max_length = 12 , blank = True )
msn = models . CharField ( _ ( ' MSN ' ) , max_length = 80 , blank = True )
aim = models . CharField ( _ ( ' AIM ' ) , max_length = 80 , blank = True )
yahoo = models . CharField ( _ ( ' Yahoo ' ) , max_length = 80 , blank = True )
location = models . CharField ( _ ( ' Location ' ) , max_length = 30 , blank = True )
2009-01-17 17:56:19 +02:00
signature = models . TextField ( _ ( ' Signature ' ) , blank = True , default = ' ' , max_length = forum_settings . SIGNATURE_MAX_LENGTH )
2012-02-17 00:52:33 +02:00
signature_html = models . TextField ( _ ( ' Signature ' ) , blank = True , default = ' ' , max_length = forum_settings . SIGNATURE_MAX_LENGTH )
2009-01-17 17:56:19 +02:00
time_zone = models . FloatField ( _ ( ' Time zone ' ) , choices = TZ_CHOICES , default = float ( forum_settings . DEFAULT_TIME_ZONE ) )
2010-02-26 17:12:49 +02:00
language = models . CharField ( _ ( ' Language ' ) , max_length = 5 , default = ' ' , choices = settings . LANGUAGES )
2009-01-17 17:56:19 +02:00
avatar = ExtendedImageField ( _ ( ' Avatar ' ) , blank = True , default = ' ' , upload_to = forum_settings . AVATARS_UPLOAD_TO , width = forum_settings . AVATAR_WIDTH , height = forum_settings . AVATAR_HEIGHT )
2009-04-21 21:55:10 +03:00
theme = models . CharField ( _ ( ' Theme ' ) , choices = THEME_CHOICES , max_length = 80 , default = ' default ' )
2009-01-05 14:30:08 +02:00
show_avatar = models . BooleanField ( _ ( ' Show avatar ' ) , blank = True , default = True )
show_signatures = models . BooleanField ( _ ( ' Show signatures ' ) , blank = True , default = True )
2010-11-08 19:15:48 +02:00
show_smilies = models . BooleanField ( _ ( ' Show smilies ' ) , blank = True , default = True )
2009-01-05 14:30:08 +02:00
privacy_permission = models . IntegerField ( _ ( ' Privacy permission ' ) , choices = PRIVACY_CHOICES , default = 1 )
2012-08-07 16:44:16 +03:00
auto_subscribe = models . BooleanField ( _ ( ' Auto subscribe ' ) , help_text = _ ( " Auto subscribe all topics you have created or reply. " ) , blank = True , default = False )
2009-01-17 17:56:19 +02:00
markup = models . CharField ( _ ( ' Default markup ' ) , max_length = 15 , default = forum_settings . DEFAULT_MARKUP , choices = MARKUP_CHOICES )
2009-01-19 21:23:53 +02:00
post_count = models . IntegerField ( _ ( ' Post count ' ) , blank = True , default = 0 )
2009-01-05 14:30:08 +02:00
2012-03-03 23:24:39 +02:00
objects = ProfileManager ( )
2009-01-05 14:30:08 +02:00
class Meta :
verbose_name = _ ( ' Profile ' )
verbose_name_plural = _ ( ' Profiles ' )
2009-01-22 14:38:28 +02:00
2009-01-05 14:30:08 +02:00
def last_post ( self ) :
2010-11-29 00:57:19 +02:00
posts = Post . objects . filter ( user__id = self . user_id ) . order_by ( ' -created ' )
2009-07-02 16:59:07 +03:00
if posts :
2009-01-05 14:30:08 +02:00
return posts [ 0 ] . created
else :
return None
2009-05-24 22:51:42 +03:00
2014-03-04 17:24:30 -05:00
def set_counts ( self ) :
self . post_count = Post . objects . filter ( user = self . user ) . count ( )
2009-10-22 15:19:26 +03:00
class PostTracking ( models . Model ) :
2009-01-05 14:30:08 +02:00
"""
2009-10-22 15:19:26 +03:00
Model for tracking read / unread posts .
In topics stored ids of topics and last_posts as dict .
2009-01-05 14:30:08 +02:00
"""
2009-10-22 15:19:26 +03:00
user = AutoOneToOneField ( User )
2012-09-24 14:18:16 +03:00
topics = JSONField ( null = True , blank = True )
last_read = models . DateTimeField ( null = True , blank = True )
2009-01-05 14:30:08 +02:00
class Meta :
2009-10-22 15:19:26 +03:00
verbose_name = _ ( ' Post tracking ' )
verbose_name_plural = _ ( ' Post tracking ' )
2009-01-05 14:30:08 +02:00
def __unicode__ ( self ) :
2009-10-22 15:19:26 +03:00
return self . user . username
2009-01-05 14:30:08 +02:00
class Report ( models . Model ) :
reported_by = models . ForeignKey ( User , related_name = ' reported_by ' , verbose_name = _ ( ' Reported by ' ) )
post = models . ForeignKey ( Post , verbose_name = _ ( ' Post ' ) )
zapped = models . BooleanField ( _ ( ' Zapped ' ) , blank = True , default = False )
2012-09-24 14:18:16 +03:00
zapped_by = models . ForeignKey ( User , related_name = ' zapped_by ' , blank = True , null = True , verbose_name = _ ( ' Zapped by ' ) )
2009-01-05 14:30:08 +02:00
created = models . DateTimeField ( _ ( ' Created ' ) , blank = True )
reason = models . TextField ( _ ( ' Reason ' ) , blank = True , default = ' ' , max_length = ' 1000 ' )
class Meta :
verbose_name = _ ( ' Report ' )
verbose_name_plural = _ ( ' Reports ' )
def __unicode__ ( self ) :
2012-09-24 14:18:16 +03:00
return u ' %s %s ' % ( self . reported_by , self . zapped )
2009-01-05 14:30:08 +02:00
2009-04-08 20:43:29 +03:00
class Ban ( models . Model ) :
2009-10-20 14:01:50 +03:00
user = models . OneToOneField ( User , verbose_name = _ ( ' Banned user ' ) , related_name = ' ban_users ' )
2013-01-30 19:15:52 -05:00
ban_start = models . DateTimeField ( _ ( ' Ban start ' ) , default = timezone . now )
2009-04-08 20:43:29 +03:00
ban_end = models . DateTimeField ( _ ( ' Ban end ' ) , blank = True , null = True )
reason = models . TextField ( _ ( ' Reason ' ) )
2009-10-20 14:01:50 +03:00
2009-04-08 20:43:29 +03:00
class Meta :
verbose_name = _ ( ' Ban ' )
verbose_name_plural = _ ( ' Bans ' )
2009-11-29 18:56:11 +02:00
def __unicode__ ( self ) :
return self . user . username
2009-04-08 20:43:29 +03:00
def save ( self , * args , * * kwargs ) :
self . user . is_active = False
self . user . save ( )
super ( Ban , self ) . save ( * args , * * kwargs )
2009-10-20 14:01:50 +03:00
def delete ( self , * args , * * kwargs ) :
self . user . is_active = True
self . user . save ( )
super ( Ban , self ) . delete ( * args , * * kwargs )
2009-04-14 14:57:17 +03:00
class Attachment ( models . Model ) :
post = models . ForeignKey ( Post , verbose_name = _ ( ' Post ' ) , related_name = ' attachments ' )
size = models . IntegerField ( _ ( ' Size ' ) )
content_type = models . CharField ( _ ( ' Content type ' ) , max_length = 255 )
path = models . CharField ( _ ( ' Path ' ) , max_length = 255 )
name = models . TextField ( _ ( ' Name ' ) )
hash = models . CharField ( _ ( ' Hash ' ) , max_length = 40 , blank = True , default = ' ' , db_index = True )
2009-11-29 18:56:11 +02:00
def __unicode__ ( self ) :
return self . name
2009-04-14 14:57:17 +03:00
def save ( self , * args , * * kwargs ) :
super ( Attachment , self ) . save ( * args , * * kwargs )
if not self . hash :
2012-04-23 14:28:40 +03:00
self . hash = sha1 ( str ( self . id ) + settings . SECRET_KEY ) . hexdigest ( )
2009-04-14 14:57:17 +03:00
super ( Attachment , self ) . save ( * args , * * kwargs )
2009-11-29 18:56:11 +02:00
@models.permalink
2009-04-14 14:57:17 +03:00
def get_absolute_url ( self ) :
2009-11-29 18:56:11 +02:00
return ( ' djangobb:forum_attachment ' , [ self . hash ] )
2009-04-14 14:57:17 +03:00
def get_absolute_path ( self ) :
return os . path . join ( settings . MEDIA_ROOT , forum_settings . ATTACHMENT_UPLOAD_TO ,
self . path )
2011-08-18 10:42:44 +03:00
2012-08-10 11:12:52 +03:00
#------------------------------------------------------------------------------
class Poll ( models . Model ) :
topic = models . ForeignKey ( Topic )
question = models . CharField ( max_length = 200 )
choice_count = models . PositiveSmallIntegerField ( default = 1 ,
help_text = _ ( " How many choices are allowed simultaneously. " ) ,
)
active = models . BooleanField ( default = True ,
help_text = _ ( " Can users vote to this poll or just see the result? " ) ,
)
deactivate_date = models . DateTimeField ( null = True , blank = True ,
help_text = _ ( " Point of time after this poll would be automatic deactivated " ) ,
)
users = models . ManyToManyField ( User , blank = True , null = True ,
help_text = _ ( " Users who has voted this poll. " ) ,
)
def auto_deactivate ( self ) :
if self . active and self . deactivate_date :
2013-01-30 19:15:52 -05:00
now = timezone . now ( )
2012-08-10 11:12:52 +03:00
if now > self . deactivate_date :
self . active = False
self . save ( )
def __unicode__ ( self ) :
return self . question
class PollChoice ( models . Model ) :
poll = models . ForeignKey ( Poll , related_name = " choices " )
choice = models . CharField ( max_length = 200 )
votes = models . IntegerField ( default = 0 , editable = False )
def percent ( self ) :
if not self . votes :
return 0.0
result = PollChoice . objects . filter ( poll = self . poll ) . aggregate ( aggregates . Sum ( " votes " ) )
votes_sum = result [ " votes__sum " ]
return float ( self . votes ) / votes_sum * 100
def __unicode__ ( self ) :
return self . choice
#------------------------------------------------------------------------------
2014-03-10 13:29:16 -04:00
class PostStatusManager ( models . Manager ) :
def create_for_post ( self , post , * * kwargs ) :
user_agent = kwargs . get ( " HTTP_USER_AGENT " , None )
referrer = kwargs . get ( " HTTP_REFERER " , None )
permalink = kwargs . get ( " permalink " , None )
return self . create (
post = post , topic = post . topic , forum = post . topic . forum ,
user_agent = user_agent , referrer = referrer , permalink = permalink )
2014-03-13 19:38:53 -04:00
def review_posts ( self , posts , certainly_spam = False ) :
for post in posts :
try :
post_status = post . poststatus
except PostStatus . DoesNotExist :
post_status = self . create_for_post ( post )
post_status . review ( certainly_spam = certainly_spam )
2015-02-26 08:24:23 -05:00
def delete_user_posts ( self , posts ) :
for post in posts :
try :
post_status = post . poststatus
except PostStatus . DoesNotExist :
post_status = self . create_for_post ( post )
post_status . filter_user_deleted ( )
def undelete_user_posts ( self , posts ) :
for post in posts :
try :
post_status = post . poststatus
except PostStatus . DoesNotExist :
post_status = self . create_for_post ( post )
post_status . filter_user_undeleted ( )
2014-03-12 20:18:44 -04:00
def review_new_posts ( self ) :
unreviewed = self . filter ( state = PostStatus . UNREVIEWED )
for post_status in unreviewed :
2014-03-13 19:38:53 -04:00
post_status . review ( )
2014-03-12 20:18:44 -04:00
return unreviewed
2014-03-10 13:29:16 -04:00
2014-02-26 17:54:17 -05:00
class PostStatus ( models . Model ) :
"""
Keeps track of the status of posts for moderation purposes .
"""
UNREVIEWED = ' unreviewed '
2015-02-26 08:24:23 -05:00
USER_DELETED = ' user_deleted '
2014-02-26 17:54:17 -05:00
FILTERED_SPAM = ' filtered_spam '
FILTERED_HAM = ' filtered_ham '
MARKED_SPAM = ' marked_spam '
MARKED_HAM = ' marked_ham '
2015-02-24 16:40:24 -05:00
AKISMET_MAX_SIZE = 1024 * 250
2014-12-30 11:34:58 -05:00
2014-03-12 10:39:39 -04:00
post = models . OneToOneField ( Post , db_index = True )
2014-10-31 12:59:48 -04:00
state = FSMField ( default = UNREVIEWED , db_index = True )
2014-03-10 13:29:16 -04:00
topic = models . ForeignKey ( Topic ) # Original topic
forum = models . ForeignKey ( Forum ) # Original forum
2014-02-26 17:54:17 -05:00
user_agent = models . CharField ( max_length = 200 , blank = True , null = True )
referrer = models . CharField ( max_length = 200 , blank = True , null = True )
permalink = models . CharField ( max_length = 200 , blank = True , null = True )
2014-03-10 13:29:16 -04:00
objects = PostStatusManager ( )
spam_category = None
spam_forum = None
spam_topic = None
def _get_spam_dustbin ( self ) :
if self . spam_category is None :
self . spam_category , _ = Category . objects . get_or_create (
2014-03-12 10:39:39 -04:00
name = forum_settings . SPAM_CATEGORY_NAME )
2014-03-10 13:29:16 -04:00
if self . spam_forum is None :
self . spam_forum , _ = Forum . objects . get_or_create (
category = self . spam_category ,
2014-03-12 10:39:39 -04:00
name = forum_settings . SPAM_FORUM_NAME )
2014-03-10 13:29:16 -04:00
if self . spam_topic is None :
filterbot = User . objects . get_by_natural_key ( " filterbot " )
self . spam_topic , _ = Topic . objects . get_or_create (
2014-03-12 10:39:39 -04:00
forum = self . spam_forum , name = forum_settings . SPAM_TOPIC_NAME ,
2014-03-10 13:29:16 -04:00
user = filterbot )
return ( self . spam_topic , self . spam_forum )
def _undelete_post ( self ) :
"""
If the post is in the spam dustbin , move it back to its original location .
"""
spam_topic , spam_forum = self . _get_spam_dustbin ( )
post = self . post
topic = self . topic
head = post . topic . head
if post == head :
2014-03-18 13:41:47 -04:00
# Move the original topic back to the original forum (either from
# the dustbin, or from the spam dustbin)
2014-03-10 13:29:16 -04:00
topic . move_to ( self . forum )
2014-03-18 13:41:47 -04:00
if topic != post . topic :
# If the post was moved from its original topic, put it back now that
# the topic is in place.
post . move_to ( topic , delete_topic = False )
2014-03-10 13:29:16 -04:00
def _delete_post ( self ) :
"""
Move the post to the spam dustbin .
"""
spam_topic , spam_forum = self . _get_spam_dustbin ( )
post = self . post
topic = self . topic
head = topic . head
if post == head :
topic . move_to ( spam_forum )
else :
post . move_to ( spam_topic )
2014-02-26 17:54:17 -05:00
def to_akismet_data ( self ) :
post = self . post
topic = self . topic
user = post . user
user_ip = post . user_ip
comment_author = user . username
user_agent = self . user_agent
referrer = self . referrer
permalink = self . permalink
comment_date_gmt = post . created . isoformat ( ' ' )
comment_post_modified_gmt = topic . created . isoformat ( ' ' )
return {
' user_ip ' : user_ip ,
' user_agent ' : user_agent ,
' comment_author ' : comment_author ,
' referrer ' : referrer ,
' permalink ' : permalink ,
' comment_type ' : ' comment ' ,
' comment_date_gmt ' : comment_date_gmt ,
' comment_post_modified_gmt ' : comment_post_modified_gmt
}
2014-12-29 10:11:31 -05:00
def to_akismet_content ( self ) :
2014-12-23 11:53:39 -05:00
"""
Truncate the post body to the largest allowed string size . Use size , not
length , since the Akismet server checks size , not length .
"""
2014-12-30 11:34:58 -05:00
return self . post . body . encode ( ' utf-8 ' ) [ : self . AKISMET_MAX_SIZE ] . decode ( ' utf-8 ' , ' ignore ' )
2014-12-23 11:53:39 -05:00
2014-03-10 13:29:16 -04:00
def _comment_check ( self ) :
2014-02-26 17:54:17 -05:00
"""
Pass the associated post through Akismet if it ' s available. If it ' s not
available return None . Otherwise return True or False .
"""
if akismet_api is None :
logger . warning ( " Skipping akismet check. No api. " )
return None
data = self . to_akismet_data ( )
2014-12-23 11:53:39 -05:00
content = self . to_akismet_content ( )
2014-02-26 17:54:17 -05:00
is_spam = None
try :
2014-03-18 13:40:49 -04:00
is_spam = akismet_api . comment_check ( content , data )
2014-09-05 16:00:55 -04:00
except AkismetError as e :
try :
# try again, in case of timeout
is_spam = akismet_api . comment_check ( content , data )
except Exception as e :
2015-02-24 16:26:37 -05:00
logger . error (
" Error while checking Akismet " , exc_info = True , extra = {
2015-02-25 10:12:41 -05:00
" post " : self . post , " post_id " : self . post . id ,
2015-02-24 16:26:37 -05:00
" content_length " : len ( content ) } )
2014-09-05 16:00:55 -04:00
is_spam = None
2014-09-05 16:07:59 -04:00
except Exception as e :
2015-02-24 16:26:37 -05:00
logger . error (
" Error while checking Akismet " , exc_info = True , extra = {
2015-02-25 10:12:41 -05:00
" post " : self . post , " post_id " : self . post . id ,
2015-02-24 16:26:37 -05:00
" content_length " : len ( content ) } )
2014-09-05 16:07:59 -04:00
is_spam = None
2014-02-26 17:54:17 -05:00
return is_spam
2014-03-10 13:29:16 -04:00
def _submit_comment ( self , report_type ) :
"""
Report this post to Akismet as spam or ham . Raises an exception if it
fails . report_type is ' spam ' or ' ham ' . Used by report_spam / report_ham .
"""
if akismet_api is None :
2015-01-02 18:31:00 -05:00
logger . error ( " Can ' t submit to Akismet. No API. " )
return None
2014-03-10 13:29:16 -04:00
data = self . to_akismet_data ( )
2014-12-23 11:53:39 -05:00
content = self . to_akismet_content ( )
2014-03-10 13:29:16 -04:00
if report_type == " spam " :
akismet_api . submit_spam ( content , data )
elif report_type == " ham " :
akismet_api . submit_ham ( content , data )
else :
raise NotImplementedError (
" You ' re trying to report an unsupported comment type. " )
def _submit_spam ( self ) :
"""
Report this post to Akismet as spam .
"""
self . _submit_comment ( " spam " )
def _submit_ham ( self ) :
"""
Report this post to Akismet as ham .
"""
self . _submit_comment ( " ham " )
2014-02-26 17:54:17 -05:00
def is_spam ( self ) :
"""
Condition used by the FSM . Return True if the Akismet API is available
and returns a positive . Otherwise return False or None .
"""
2014-03-10 13:29:16 -04:00
is_spam = self . _comment_check ( )
2014-02-26 17:54:17 -05:00
if is_spam is None :
return False
else :
return is_spam
def is_ham ( self ) :
"""
Inverse of is_spam .
"""
2014-03-10 13:29:16 -04:00
is_spam = self . _comment_check ( )
2014-02-26 17:54:17 -05:00
if is_spam is None :
return False
else :
return not is_spam
@transition (
field = state , source = UNREVIEWED , target = FILTERED_SPAM ,
save = True , conditions = [ is_spam ] )
def filter_spam ( self ) :
"""
2014-03-10 13:29:16 -04:00
Akismet detected this post is spam , move it to the dustbin and report it .
2014-02-26 17:54:17 -05:00
"""
2014-03-12 10:39:39 -04:00
self . _delete_post ( )
2014-02-26 17:54:17 -05:00
@transition (
field = state , source = UNREVIEWED , target = FILTERED_HAM ,
save = True , conditions = [ is_ham ] )
def filter_ham ( self ) :
"""
2014-03-10 13:29:16 -04:00
Akismet detected this post as ham . Don ' t do anything (except change state).
2014-02-26 17:54:17 -05:00
"""
pass
2015-02-26 08:24:23 -05:00
@transition (
2015-02-26 09:32:49 -05:00
field = state , source = [ UNREVIEWED , FILTERED_HAM , MARKED_HAM ] , target = USER_DELETED ,
2015-02-26 08:24:23 -05:00
save = True )
def filter_user_deleted ( self ) :
"""
Post is not marked spam by akismet , but user has been globally deleted ,
putting this into the spam dusbin .
"""
self . _delete_post ( )
2014-02-26 17:54:17 -05:00
@transition (
field = state , source = [ FILTERED_SPAM , MARKED_SPAM ] , target = MARKED_HAM ,
save = True )
def mark_ham ( self ) :
"""
Either Akismet returned a false positive , or a moderator accidentally
2014-03-10 13:29:16 -04:00
marked this as spam . Tell Akismet that this is ham , undelete it .
2014-02-26 17:54:17 -05:00
"""
2014-03-10 13:29:16 -04:00
self . _submit_ham ( )
self . _undelete_post ( )
2014-02-26 17:54:17 -05:00
@transition (
field = state , source = [ FILTERED_HAM , MARKED_HAM ] , target = MARKED_SPAM ,
save = True )
def mark_spam ( self ) :
"""
Akismet missed this , or a moderator accidentally marked it as ham . Tell
Akismet that this is spam .
"""
2014-03-10 13:29:16 -04:00
self . _submit_spam ( )
2014-03-12 10:39:39 -04:00
self . _delete_post ( )
2014-02-26 17:54:17 -05:00
2015-02-26 08:24:23 -05:00
@transition (
field = state , source = USER_DELETED , target = UNREVIEWED ,
save = True )
def filter_user_undeleted ( self ) :
"""
Post is not marked spam by akismet , but user has been globally deleted ,
putting this into the spam dusbin .
"""
self . _undelete_post ( )
2014-03-13 19:38:53 -04:00
def review ( self , certainly_spam = False ) :
"""
Process this post , used by the manager and the spam - hammer . The
` ` certainly_spam ` ` argument is used to force mark as spam / delete the
post , no matter what status Akismet returns .
"""
if can_proceed ( self . filter_spam ) :
self . filter_spam ( )
elif can_proceed ( self . filter_ham ) :
self . filter_ham ( )
if certainly_spam :
2014-03-20 11:50:39 -04:00
self . mark_spam ( )
2014-03-13 19:38:53 -04:00
else :
if certainly_spam :
self . _delete_post ( )
2015-02-24 16:26:37 -05:00
logger . warn (
" Couldn ' t filter post. " , exc_info = True , extra = {
' post_id ' : self . post . id , ' content_length ' : len ( self . post . body ) } )
2014-03-13 19:38:53 -04:00
2014-02-26 17:54:17 -05:00
2011-08-18 10:42:44 +03:00
from . signals import post_saved , topic_saved
post_save . connect ( post_saved , sender = Post , dispatch_uid = ' djangobb_post_save ' )
post_save . connect ( topic_saved , sender = Topic , dispatch_uid = ' djangobb_topic_save ' )