| 1 |
from datetime import datetime, timedelta |
|---|
| 2 |
from django.db import models |
|---|
| 3 |
from django.db.models import Q |
|---|
| 4 |
from django.db.models import signals |
|---|
| 5 |
from django.core.urlresolvers import reverse |
|---|
| 6 |
from django.core.mail import send_mass_mail |
|---|
| 7 |
from django.core.cache import cache |
|---|
| 8 |
from django.utils.safestring import mark_safe |
|---|
| 9 |
from django.utils.translation import ugettext as _ |
|---|
| 10 |
from django.template.context import RequestContext |
|---|
| 11 |
from django.template import loader |
|---|
| 12 |
from django.conf import settings |
|---|
| 13 |
from django.contrib.auth.models import User |
|---|
| 14 |
from django import forms |
|---|
| 15 |
|
|---|
| 16 |
import sphene.community.signals |
|---|
| 17 |
from sphene.community.middleware import get_current_request, get_current_user, get_current_group, get_current_session |
|---|
| 18 |
from sphene.community.sphutils import sphpermalink, get_urlconf, get_sph_setting, get_method_by_name |
|---|
| 19 |
from sphene.community.signals import profile_edit_init_form, profile_edit_save_form, profile_display |
|---|
| 20 |
from sphene.community.permissionutils import has_permission_flag |
|---|
| 21 |
from sphene.community.forms import EditProfileForm, Separator |
|---|
| 22 |
from sphene.community.models import Group |
|---|
| 23 |
from sphene.sphblog.utils import slugify |
|---|
| 24 |
from sphene.sphboard import categorytyperegistry |
|---|
| 25 |
from renderers import POST_MARKUP_CHOICES, render_body |
|---|
| 26 |
|
|---|
| 27 |
import logging |
|---|
| 28 |
logger = logging.getLogger('sphene.sphboard.models') |
|---|
| 29 |
|
|---|
| 30 |
""" |
|---|
| 31 |
Extended Group methods ........ |
|---|
| 32 |
""" |
|---|
| 33 |
|
|---|
| 34 |
def has_monitor(self): |
|---|
| 35 |
return self.__get_monitor(get_current_user()) |
|---|
| 36 |
|
|---|
| 37 |
def has_direct_monitor(self): |
|---|
| 38 |
return self.__get_monitor(get_current_user()) |
|---|
| 39 |
|
|---|
| 40 |
def toggle_monitor(self): |
|---|
| 41 |
"""Toggles monitor and returns the newly created monitor, or None if an |
|---|
| 42 |
existing monitor was deleted.""" |
|---|
| 43 |
if self.has_direct_monitor(): |
|---|
| 44 |
self.has_direct_monitor().delete() |
|---|
| 45 |
if hasattr(self, '__monitor'): delattr(self,'__monitor') |
|---|
| 46 |
else: |
|---|
| 47 |
monitor = Monitor(user = get_current_user(), |
|---|
| 48 |
group = self, ) |
|---|
| 49 |
monitor.save() |
|---|
| 50 |
self.__monitor = monitor |
|---|
| 51 |
return monitor |
|---|
| 52 |
|
|---|
| 53 |
def __get_monitor(self, user): |
|---|
| 54 |
if hasattr(self, '__monitor'): return self.__monitor |
|---|
| 55 |
|
|---|
| 56 |
try: |
|---|
| 57 |
monitor = Monitor.objects.get( user = user, |
|---|
| 58 |
group = self, |
|---|
| 59 |
category__isnull = True, |
|---|
| 60 |
thread__isnull = True, ) |
|---|
| 61 |
except Monitor.DoesNotExist: |
|---|
| 62 |
monitor = None |
|---|
| 63 |
return monitor |
|---|
| 64 |
|
|---|
| 65 |
Group.has_monitor = has_monitor |
|---|
| 66 |
Group.has_direct_monitor = has_direct_monitor |
|---|
| 67 |
Group.toggle_monitor = toggle_monitor |
|---|
| 68 |
Group.__get_monitor = __get_monitor |
|---|
| 69 |
|
|---|
| 70 |
""" |
|---|
| 71 |
END of extended Group methods ... |
|---|
| 72 |
""" |
|---|
| 73 |
|
|---|
| 74 |
POSTS_ALLOWED_CHOICES = ( |
|---|
| 75 |
(-1, 'All Users'), |
|---|
| 76 |
(0, 'Loggedin Users'), |
|---|
| 77 |
(1, 'Members of the Group'), |
|---|
| 78 |
(2, 'Staff Members'), |
|---|
| 79 |
(3, 'Nobody'), |
|---|
| 80 |
) |
|---|
| 81 |
|
|---|
| 82 |
class AccessCategoryManager(models.Manager): |
|---|
| 83 |
def filter_for_group(self, group): |
|---|
| 84 |
user = get_current_user() |
|---|
| 85 |
level = -1 |
|---|
| 86 |
if user.is_authenticated(): |
|---|
| 87 |
level = 0 |
|---|
| 88 |
if user.is_superuser: |
|---|
| 89 |
level = 2 |
|---|
| 90 |
elif group and group.get_member(user) != None: |
|---|
| 91 |
level = 3 |
|---|
| 92 |
return self.filter(group = group, |
|---|
| 93 |
allowview__lte = level) |
|---|
| 94 |
|
|---|
| 95 |
def rolemember_limitation_objects(self, group): |
|---|
| 96 |
return self.filter( group = group ) |
|---|
| 97 |
|
|---|
| 98 |
|
|---|
| 99 |
class CategoryTypeChoices(object): |
|---|
| 100 |
def __iter__(self): |
|---|
| 101 |
if get_current_request() is None: |
|---|
| 102 |
return [].__iter__() |
|---|
| 103 |
choices = () |
|---|
| 104 |
try: |
|---|
| 105 |
for ct in categorytyperegistry.get_category_type_list(): |
|---|
| 106 |
choices += ((ct.name, "%s (%s)" % (unicode(ct.label), ct.name)),) |
|---|
| 107 |
except: |
|---|
| 108 |
|
|---|
| 109 |
|
|---|
| 110 |
|
|---|
| 111 |
print "Error while trying to fetch category types." |
|---|
| 112 |
pass |
|---|
| 113 |
|
|---|
| 114 |
return choices.__iter__() |
|---|
| 115 |
|
|---|
| 116 |
|
|---|
| 117 |
def get_category_type_choices(): |
|---|
| 118 |
""" |
|---|
| 119 |
This method generates the choices for the 'category_type' |
|---|
| 120 |
field of Category entity - this should probably be improved |
|---|
| 121 |
to make sure that this method is fully dynamic.. |
|---|
| 122 |
""" |
|---|
| 123 |
return CategoryTypeChoices() |
|---|
| 124 |
|
|---|
| 125 |
def get_tags_for_categories(categories): |
|---|
| 126 |
""" |
|---|
| 127 |
Returns a list of all used tags in the given categories. |
|---|
| 128 |
""" |
|---|
| 129 |
from django.contrib.contenttypes.models import ContentType |
|---|
| 130 |
from sphene.community.models import Tag, TagLabel, TaggedItem |
|---|
| 131 |
from django.db import connection |
|---|
| 132 |
|
|---|
| 133 |
group_ids = list() |
|---|
| 134 |
category_ids = list() |
|---|
| 135 |
for category in categories: |
|---|
| 136 |
group = category.group |
|---|
| 137 |
if not group.id in group_ids: |
|---|
| 138 |
group_ids.append(group.id) |
|---|
| 139 |
category_ids.append(category.id) |
|---|
| 140 |
|
|---|
| 141 |
tags = Tag.objects.filter( group__id__in = group_ids ) |
|---|
| 142 |
|
|---|
| 143 |
qn = connection.ops.quote_name |
|---|
| 144 |
|
|---|
| 145 |
content_type = ContentType.objects.get_for_model(Post) |
|---|
| 146 |
|
|---|
| 147 |
tags = tags.extra( |
|---|
| 148 |
tables=[TagLabel._meta.db_table, |
|---|
| 149 |
TaggedItem._meta.db_table, |
|---|
| 150 |
Post._meta.db_table, ], |
|---|
| 151 |
where=[ |
|---|
| 152 |
'%s.tag_id = %s.%s' % (qn(TagLabel._meta.db_table), |
|---|
| 153 |
qn(Tag._meta.db_table), |
|---|
| 154 |
Tag._meta.pk.column), |
|---|
| 155 |
'%s.tag_label_id = %s.%s' % (qn(TaggedItem._meta.db_table), |
|---|
| 156 |
qn(TagLabel._meta.db_table), |
|---|
| 157 |
TagLabel._meta.pk.column), |
|---|
| 158 |
'%s.content_type_id = %%s' % (qn(TaggedItem._meta.db_table)), |
|---|
| 159 |
'%s.object_id = %s.%s' % (qn(TaggedItem._meta.db_table), |
|---|
| 160 |
qn(Post._meta.db_table), |
|---|
| 161 |
Post._meta.pk.column), |
|---|
| 162 |
'%s.category_id IN (%s)' % (qn(Post._meta.db_table), ','.join([str(cid) for cid in category_ids ])), |
|---|
| 163 |
], |
|---|
| 164 |
params=[content_type.pk],).order_by('name').distinct() |
|---|
| 165 |
|
|---|
| 166 |
|
|---|
| 167 |
return tags |
|---|
| 168 |
|
|---|
| 169 |
|
|---|
| 170 |
def get_all_viewable_categories(group, user): |
|---|
| 171 |
""" |
|---|
| 172 |
returns a list containing the IDs of all categories viewable by the given |
|---|
| 173 |
user in the given group. |
|---|
| 174 |
""" |
|---|
| 175 |
all_categories = Category.objects.filter( group = group ) |
|---|
| 176 |
allowed_categories = list() |
|---|
| 177 |
for category in all_categories: |
|---|
| 178 |
if category.has_view_permission( user ): |
|---|
| 179 |
allowed_categories.append(category.id) |
|---|
| 180 |
return allowed_categories |
|---|
| 181 |
|
|---|
| 182 |
|
|---|
| 183 |
class Category(models.Model): |
|---|
| 184 |
name = models.CharField(max_length = 250) |
|---|
| 185 |
group = models.ForeignKey(Group, null = True, blank = True) |
|---|
| 186 |
parent = models.ForeignKey('self', related_name = 'subcategories', null = True, blank = True) |
|---|
| 187 |
description = models.TextField(blank = True) |
|---|
| 188 |
allowview = models.IntegerField( default = -1, choices = POSTS_ALLOWED_CHOICES ) |
|---|
| 189 |
allowthreads = models.IntegerField( default = 0, choices = POSTS_ALLOWED_CHOICES ) |
|---|
| 190 |
allowreplies = models.IntegerField( default = 0, choices = POSTS_ALLOWED_CHOICES ) |
|---|
| 191 |
sortorder = models.IntegerField( default = 0, null = False ) |
|---|
| 192 |
|
|---|
| 193 |
category_type = models.CharField(max_length = 250, blank = True, db_index = True, choices = get_category_type_choices()) |
|---|
| 194 |
|
|---|
| 195 |
objects = AccessCategoryManager() |
|---|
| 196 |
sph_objects = AccessCategoryManager() |
|---|
| 197 |
|
|---|
| 198 |
|
|---|
| 199 |
changelog = ( ( '2007-04-14 00', 'alter', 'ADD sortorder INTEGER' ), |
|---|
| 200 |
( '2007-04-14 01', 'update', 'SET sortorder = 0' ), |
|---|
| 201 |
( '2007-04-14 02', 'alter', 'ALTER sortorder SET NOT NULL' ), |
|---|
| 202 |
|
|---|
| 203 |
( '2007-09-03 00', 'alter', 'ADD category_type varchar(250)' ), |
|---|
| 204 |
( '2007-09-03 01', 'update', "SET category_type = ''" ), |
|---|
| 205 |
( '2007-09-03 02', 'alter', 'ALTER category_type SET NOT NULL' ), |
|---|
| 206 |
) |
|---|
| 207 |
|
|---|
| 208 |
sph_permission_flags = { 'sphboard_editallposts': |
|---|
| 209 |
'Allow editing of all posts.', |
|---|
| 210 |
|
|---|
| 211 |
'sphboard_annotate': |
|---|
| 212 |
'Allow annotating users posts.', |
|---|
| 213 |
|
|---|
| 214 |
'sphboard_move': |
|---|
| 215 |
'Allow moving of users posts.', |
|---|
| 216 |
|
|---|
| 217 |
'sphboard_sticky': |
|---|
| 218 |
'Allow marking threads as sticky.', |
|---|
| 219 |
|
|---|
| 220 |
'sphboard_lock': |
|---|
| 221 |
'Allow locking of threads.', |
|---|
| 222 |
|
|---|
| 223 |
'sphboard_post_threads': |
|---|
| 224 |
'Allow creating new threads.', |
|---|
| 225 |
|
|---|
| 226 |
'sphboard_post_replies': |
|---|
| 227 |
'Allow posting of replies to existing threads.', |
|---|
| 228 |
|
|---|
| 229 |
'sphboard_view': |
|---|
| 230 |
'Allows viewing of threads.', |
|---|
| 231 |
} |
|---|
| 232 |
|
|---|
| 233 |
def get_category_type(self): |
|---|
| 234 |
if not self.category_type or self.category_type == 'None': |
|---|
| 235 |
from sphene.sphboard.categorytypes import DefaultCategoryType |
|---|
| 236 |
return DefaultCategoryType( self ) |
|---|
| 237 |
ct = categorytyperegistry.get_category_type( self.category_type ) |
|---|
| 238 |
if ct is None: |
|---|
| 239 |
raise Exception( 'Invalid category type "%s" for "%s"' % (self.category_type, self.name)) |
|---|
| 240 |
return ct(self) |
|---|
| 241 |
|
|---|
| 242 |
def get_rolemember_limitation_objects(group): |
|---|
| 243 |
""" |
|---|
| 244 |
Tells sphene community objects that this model can be used to limit |
|---|
| 245 |
the membership of a user in a given role. |
|---|
| 246 |
""" |
|---|
| 247 |
return Category.objects.filter( group = group ) |
|---|
| 248 |
|
|---|
| 249 |
def get_children(self): |
|---|
| 250 |
""" Returns all children of this category in which the user has view permission. """ |
|---|
| 251 |
return Category.sph_objects.filter_for_group( self.group ).filter( parent = self ) |
|---|
| 252 |
|
|---|
| 253 |
def canContainPosts(self): |
|---|
| 254 |
return self.allowthreads != 3 |
|---|
| 255 |
|
|---|
| 256 |
def is_private(self): |
|---|
| 257 |
return self.category_type == 'privatemessages' |
|---|
| 258 |
|
|---|
| 259 |
@property |
|---|
| 260 |
def posts(self): |
|---|
| 261 |
posts = self._posts |
|---|
| 262 |
if self.is_private(): |
|---|
| 263 |
user = get_current_user() |
|---|
| 264 |
recps = PostRecipient.objects.filter(user=user).values('post').query |
|---|
| 265 |
posts = posts.filter(Q(id__in=recps) | Q(author=user)) |
|---|
| 266 |
|
|---|
| 267 |
return posts |
|---|
| 268 |
|
|---|
| 269 |
def get_thread_list(self): |
|---|
| 270 |
|
|---|
| 271 |
if get_sph_setting( 'workaround_select_related_bug' ): |
|---|
| 272 |
|
|---|
| 273 |
return self.threadinformation_set |
|---|
| 274 |
qs = self.threadinformation_set.filter(root_post__is_hidden = 0).select_related( depth = 1 ) |
|---|
| 275 |
|
|---|
| 276 |
if self.is_private(): |
|---|
| 277 |
user = get_current_user() |
|---|
| 278 |
recps = PostRecipient.objects.filter(user=user).values('post').query |
|---|
| 279 |
qs = qs.filter(Q(latest_post__id__in=recps) | Q(root_post__author=user)) |
|---|
| 280 |
print qs.query |
|---|
| 281 |
|
|---|
| 282 |
return qs |
|---|
| 283 |
|
|---|
| 284 |
def threadCount(self): |
|---|
| 285 |
return self.threadinformation_set.count() |
|---|
| 286 |
|
|---|
| 287 |
def postCount(self): |
|---|
| 288 |
return self.posts.count() |
|---|
| 289 |
|
|---|
| 290 |
def get_latest_post(self): |
|---|
| 291 |
return self.posts.latest( 'postdate' ) |
|---|
| 292 |
|
|---|
| 293 |
|
|---|
| 294 |
latestPost = get_latest_post |
|---|
| 295 |
|
|---|
| 296 |
def has_post_thread_permission(self, user = None): |
|---|
| 297 |
if not user: |
|---|
| 298 |
user = get_current_user() |
|---|
| 299 |
return self.testAllowance(user, self.allowthreads) \ |
|---|
| 300 |
or has_permission_flag(user, 'sphboard_post_threads', self) |
|---|
| 301 |
allowPostThread = has_post_thread_permission |
|---|
| 302 |
|
|---|
| 303 |
def has_view_permission(self, user = None): |
|---|
| 304 |
if not user: |
|---|
| 305 |
user = get_current_user() |
|---|
| 306 |
return self.testAllowance(user, self.allowview) \ |
|---|
| 307 |
or has_permission_flag(user, 'sphboard_view', self) |
|---|
| 308 |
|
|---|
| 309 |
def testAllowance(self, user, level): |
|---|
| 310 |
if level == -1: |
|---|
| 311 |
return True; |
|---|
| 312 |
if user == None or not user.is_authenticated(): |
|---|
| 313 |
return False; |
|---|
| 314 |
if level == 0: |
|---|
| 315 |
return True; |
|---|
| 316 |
|
|---|
| 317 |
if level == 1 and self.group.get_member(user) != None: |
|---|
| 318 |
return True |
|---|
| 319 |
|
|---|
| 320 |
if level <= 2 and user.is_staff: |
|---|
| 321 |
return True |
|---|
| 322 |
|
|---|
| 323 |
return user.has_perm( 'sphboard.add_post' ); |
|---|
| 324 |
|
|---|
| 325 |
def has_permission_flag(self, user, flag): |
|---|
| 326 |
return False |
|---|
| 327 |
|
|---|
| 328 |
def has_new_posts(self): |
|---|
| 329 |
ret = self.hasNewPosts() |
|---|
| 330 |
return ret |
|---|
| 331 |
|
|---|
| 332 |
def catchup(self, session, user): |
|---|
| 333 |
"""Marks all posts in the current category as read.""" |
|---|
| 334 |
ThreadLastVisit.objects.filter( user = user, |
|---|
| 335 |
thread__category = self, ).delete() |
|---|
| 336 |
try: |
|---|
| 337 |
categoryLastVisit = CategoryLastVisit.objects.get( category = self, |
|---|
| 338 |
user = user, ) |
|---|
| 339 |
categoryLastVisit.lastvisit = datetime.today() |
|---|
| 340 |
categoryLastVisit.oldlastvisit = None |
|---|
| 341 |
categoryLastVisit.save() |
|---|
| 342 |
except CategoryLastVisit.DoesNotExist: |
|---|
| 343 |
return True |
|---|
| 344 |
|
|---|
| 345 |
def touch(self, session, user): |
|---|
| 346 |
""" |
|---|
| 347 |
Touches the category object by updating 'lastVisit' |
|---|
| 348 |
Returns the datetime object of when it was last visited. |
|---|
| 349 |
""" |
|---|
| 350 |
|
|---|
| 351 |
if getattr(self, '_touched', False): return self._lastVisit |
|---|
| 352 |
self._touched = True |
|---|
| 353 |
self.__hasNewPosts = self._hasNewPosts(session, user) |
|---|
| 354 |
if not user.is_authenticated(): return None |
|---|
| 355 |
try: |
|---|
| 356 |
lastVisit = CategoryLastVisit.objects.get( category = self, user = user ) |
|---|
| 357 |
|
|---|
| 358 |
if not lastVisit.oldlastvisit: |
|---|
| 359 |
if self.__hasNewPosts: |
|---|
| 360 |
|
|---|
| 361 |
lastVisit.oldlastvisit = lastVisit.lastvisit |
|---|
| 362 |
|
|---|
| 363 |
except CategoryLastVisit.DoesNotExist: |
|---|
| 364 |
lastVisit = CategoryLastVisit(user = user, category = self) |
|---|
| 365 |
lastVisit.lastvisit = datetime.today() |
|---|
| 366 |
self._lastVisit = lastVisit.oldlastvisit or lastVisit.lastvisit |
|---|
| 367 |
lastVisit.save() |
|---|
| 368 |
return self._lastVisit |
|---|
| 369 |
|
|---|
| 370 |
def hasNewPosts(self): |
|---|
| 371 |
return self._hasNewPosts(get_current_session(), get_current_user()) |
|---|
| 372 |
|
|---|
| 373 |
def _hasNewPosts(self, session, user): |
|---|
| 374 |
if hasattr(self, '__hasNewPosts'): return self.__hasNewPosts |
|---|
| 375 |
if not user.is_authenticated(): return False |
|---|
| 376 |
try: |
|---|
| 377 |
latestPost = Post.objects.filter( category = self ).latest( 'postdate' ) |
|---|
| 378 |
except Post.DoesNotExist: |
|---|
| 379 |
return False |
|---|
| 380 |
|
|---|
| 381 |
|
|---|
| 382 |
try: |
|---|
| 383 |
lastVisit = CategoryLastVisit.objects.get( category = self, user = user ) |
|---|
| 384 |
except CategoryLastVisit.DoesNotExist: |
|---|
| 385 |
return False |
|---|
| 386 |
|
|---|
| 387 |
|
|---|
| 388 |
|
|---|
| 389 |
|
|---|
| 390 |
lastvisit = lastVisit.oldlastvisit or lastVisit.lastvisit |
|---|
| 391 |
|
|---|
| 392 |
if lastvisit > latestPost.postdate: |
|---|
| 393 |
return False |
|---|
| 394 |
|
|---|
| 395 |
|
|---|
| 396 |
allNewPosts = Post.objects.filter( category = self, |
|---|
| 397 |
postdate__gt = lastvisit, ) |
|---|
| 398 |
|
|---|
| 399 |
for post in allNewPosts: |
|---|
| 400 |
threadid = post.thread and post.thread.id or post.id |
|---|
| 401 |
try: |
|---|
| 402 |
lasthit = ThreadLastVisit.objects.filter( user = user, |
|---|
| 403 |
thread__id = threadid, )[0] |
|---|
| 404 |
except IndexError: |
|---|
| 405 |
return True |
|---|
| 406 |
if lasthit.lastvisit < post.postdate: |
|---|
| 407 |
return True |
|---|
| 408 |
|
|---|
| 409 |
|
|---|
| 410 |
ThreadLastVisit.objects.filter( user = user, |
|---|
| 411 |
thread__category = self, ).delete() |
|---|
| 412 |
lastVisit.oldlastvisit = None |
|---|
| 413 |
lastVisit.save() |
|---|
| 414 |
return False |
|---|
| 415 |
|
|---|
| 416 |
def toggle_monitor(self): |
|---|
| 417 |
"""Either creates a monitor if there is none currently, or deletes an |
|---|
| 418 |
existing monitor.""" |
|---|
| 419 |
|
|---|
| 420 |
if self.has_direct_monitor(): |
|---|
| 421 |
self.__get_monitor(get_current_user()).delete() |
|---|
| 422 |
if hasattr(self, '__monitor'): delattr(self,'__monitor') |
|---|
| 423 |
else: |
|---|
| 424 |
monitor = Monitor(group = self.group, |
|---|
| 425 |
user = get_current_user(), |
|---|
| 426 |
category = self) |
|---|
| 427 |
monitor.save() |
|---|
| 428 |
self.__monitor = monitor |
|---|
| 429 |
return monitor |
|---|
| 430 |
|
|---|
| 431 |
def has_monitor(self): |
|---|
| 432 |
"""Returns True if there is a monitor for |
|---|
| 433 |
the current user in the current category or any parent category.""" |
|---|
| 434 |
monitor = self.__get_monitor(get_current_user()) |
|---|
| 435 |
return monitor |
|---|
| 436 |
|
|---|
| 437 |
def has_direct_monitor(self): |
|---|
| 438 |
"""Only return True if there is a direct monitor for the current |
|---|
| 439 |
category.""" |
|---|
| 440 |
monitor = self.__get_monitor(get_current_user()) |
|---|
| 441 |
return monitor and monitor.category == self |
|---|
| 442 |
|
|---|
| 443 |
def __get_monitor(self, user): |
|---|
| 444 |
if hasattr(self, '__monitor'): return self.__monitor |
|---|
| 445 |
try: |
|---|
| 446 |
monitor = Monitor.objects.get( category = self, |
|---|
| 447 |
user = user, |
|---|
| 448 |
thread__isnull = True, ) |
|---|
| 449 |
except Monitor.DoesNotExist: |
|---|
| 450 |
if self.parent: |
|---|
| 451 |
monitor = self.parent.has_monitor() |
|---|
| 452 |
else: |
|---|
| 453 |
monitor = self.group.has_monitor() |
|---|
| 454 |
|
|---|
| 455 |
self.__monitor = monitor |
|---|
| 456 |
return self.__monitor |
|---|
| 457 |
|
|---|
| 458 |
def get_absolute_url(self): |
|---|
| 459 |
cturl = self.get_category_type().get_absolute_url_for_category() |
|---|
| 460 |
if cturl: |
|---|
| 461 |
return cturl |
|---|
| 462 |
return self._get_absolute_url() |
|---|
| 463 |
|
|---|
| 464 |
def _get_absolute_url(self): |
|---|
| 465 |
kwargs = { 'groupName': self.group.name, |
|---|
| 466 |
'category_id': self.id } |
|---|
| 467 |
if get_sph_setting('board_slugify_links'): |
|---|
| 468 |
kwargs['slug'] = slugify(self.name) or '_' |
|---|
| 469 |
name = 'sphboard_show_category' |
|---|
| 470 |
else: |
|---|
| 471 |
name = 'sphboard_show_category_without_slug' |
|---|
| 472 |
return (name, (), kwargs) |
|---|
| 473 |
_get_absolute_url = sphpermalink(_get_absolute_url) |
|---|
| 474 |
|
|---|
| 475 |
def get_absolute_post_thread_url(self): |
|---|
| 476 |
return ('sphboard_post_thread', (), { 'groupName': self.group.name, 'category_id': self.id }) |
|---|
| 477 |
get_absolute_post_thread_url = sphpermalink(get_absolute_post_thread_url) |
|---|
| 478 |
|
|---|
| 479 |
def get_absolute_url_rss_latest_threads(self): |
|---|
| 480 |
""" Returns the absolute url to the RSS feed displaying the latest threads. |
|---|
| 481 |
This will only work since django changeset 4901 (>0.96) """ |
|---|
| 482 |
return reverse( 'sphboard-feeds', |
|---|
| 483 |
urlconf = get_urlconf(), |
|---|
| 484 |
kwargs = { 'url': 'latest/%d' % self.id } ) |
|---|
| 485 |
|
|---|
| 486 |
def get_absolute_latest_url(self): |
|---|
| 487 |
return ('sphboard_latest', (), { 'groupName': self.group.name, 'category_id': self.id, }) |
|---|
| 488 |
get_absolute_latest_url = sphpermalink(get_absolute_latest_url) |
|---|
| 489 |
|
|---|
| 490 |
def get_absolute_togglemonitor_url(self): |
|---|
| 491 |
return ('sphene.sphboard.views.toggle_monitor', (), { 'groupName': self.group.name, 'monitortype': 'category', 'object_id': self.id, }) |
|---|
| 492 |
get_absolute_togglemonitor_url = sphpermalink(get_absolute_togglemonitor_url) |
|---|
| 493 |
|
|---|
| 494 |
def __unicode__(self): |
|---|
| 495 |
return self.name; |
|---|
| 496 |
|
|---|
| 497 |
class Meta: |
|---|
| 498 |
ordering = ['sortorder'] |
|---|
| 499 |
|
|---|
| 500 |
|
|---|
| 501 |
class ThreadLastVisit(models.Model): |
|---|
| 502 |
""" Entity which stores when a thread was last read. """ |
|---|
| 503 |
user = models.ForeignKey(User) |
|---|
| 504 |
lastvisit = models.DateTimeField() |
|---|
| 505 |
thread = models.ForeignKey('Post') |
|---|
| 506 |
|
|---|
| 507 |
class Meta: |
|---|
| 508 |
unique_together = (( "user", "thread", ),) |
|---|
| 509 |
|
|---|
| 510 |
|
|---|
| 511 |
class CategoryLastVisit(models.Model): |
|---|
| 512 |
""" Entity which stores when a category was last accessed. """ |
|---|
| 513 |
user = models.ForeignKey(User) |
|---|
| 514 |
lastvisit = models.DateTimeField() |
|---|
| 515 |
oldlastvisit = models.DateTimeField(null = True,) |
|---|
| 516 |
category = models.ForeignKey(Category) |
|---|
| 517 |
|
|---|
| 518 |
|
|---|
| 519 |
changelog = ( ( '2007-06-15 00', 'alter', 'ADD oldlastvisit timestamp with time zone' ), |
|---|
| 520 |
) |
|---|
| 521 |
|
|---|
| 522 |
class Meta: |
|---|
| 523 |
unique_together = ('user', 'category') |
|---|
| 524 |
|
|---|
| 525 |
|
|---|
| 526 |
class PostManager(models.Manager): |
|---|
| 527 |
""" |
|---|
| 528 |
This custom manager makes sure that only visible posts are selected |
|---|
| 529 |
(ie is_hidden has to be 0) |
|---|
| 530 |
""" |
|---|
| 531 |
def get_query_set(self): |
|---|
| 532 |
return super(PostManager, self).get_query_set().filter(is_hidden = 0) |
|---|
| 533 |
|
|---|
| 534 |
|
|---|
| 535 |
POST_STATUS_DEFAULT = 0 |
|---|
| 536 |
POST_STATUS_STICKY = 1 |
|---|
| 537 |
POST_STATUS_CLOSED = 2 |
|---|
| 538 |
POST_STATUS_POLL = 4 |
|---|
| 539 |
POST_STATUS_ANNOTATED = 8 |
|---|
| 540 |
POST_STATUS_NEW = 16 |
|---|
| 541 |
|
|---|
| 542 |
POST_STATUSES = { |
|---|
| 543 |
'default': 0, |
|---|
| 544 |
'sticky': 1, |
|---|
| 545 |
'closed': 2, |
|---|
| 546 |
|
|---|
| 547 |
'poll': 4, |
|---|
| 548 |
'annotated': 8, |
|---|
| 549 |
|
|---|
| 550 |
|
|---|
| 551 |
|
|---|
| 552 |
|
|---|
| 553 |
|
|---|
| 554 |
'new': 16, |
|---|
| 555 |
} |
|---|
| 556 |
|
|---|
| 557 |
|
|---|
| 558 |
class Post(models.Model): |
|---|
| 559 |
""" |
|---|
| 560 |
A Post object can either represent a new thread (in this case |
|---|
| 561 |
thread is None and there exists a ThreadInformation model) or a reply within a thread. |
|---|
| 562 |
|
|---|
| 563 |
if anything has to be done when a new post is created it is important to make sure that |
|---|
| 564 |
'is_hidden' is 0 - if it is non-0 it is not really created right now. |
|---|
| 565 |
""" |
|---|
| 566 |
status = models.IntegerField(default = 0, editable = False ) |
|---|
| 567 |
category = models.ForeignKey(Category, related_name = '_posts', editable = False ) |
|---|
| 568 |
subject = models.CharField(max_length = 250) |
|---|
| 569 |
icon = models.CharField(max_length=24, default='standard') |
|---|
| 570 |
body = models.TextField() |
|---|
| 571 |
thread = models.ForeignKey('self', null = True, editable = False ) |
|---|
| 572 |
postdate = models.DateTimeField( auto_now_add = True, editable = False ) |
|---|
| 573 |
author = models.ForeignKey(User, editable = False, null = True, blank = True, related_name = 'sphboard_post_author_set' ) |
|---|
| 574 |
author_ip = models.CharField(max_length = 39, null = True, blank = True) |
|---|
| 575 |
markup = models.CharField(max_length = 250, |
|---|
| 576 |
null = True, |
|---|
| 577 |
choices = POST_MARKUP_CHOICES, ) |
|---|
| 578 |
|
|---|
| 579 |
|
|---|
| 580 |
|
|---|
| 581 |
|
|---|
| 582 |
|
|---|
| 583 |
is_hidden = models.IntegerField(default = 0, editable = False, db_index = True ) |
|---|
| 584 |
|
|---|
| 585 |
|
|---|
| 586 |
allobjects = models.Manager() |
|---|
| 587 |
|
|---|
| 588 |
objects = PostManager() |
|---|
| 589 |
|
|---|
| 590 |
changelog = ( ( '2007-04-07 00', 'alter', 'ALTER author_id DROP NOT NULL', ), |
|---|
| 591 |
( '2007-06-16 00', 'alter', 'ADD markup varchar(250) NULL', ), |
|---|
| 592 |
( '2008-01-06 00', 'alter', 'ADD is_hidden INTEGER', ), |
|---|
| 593 |
( '2008-01-06 01', 'update', 'SET is_hidden = 0', ), |
|---|
| 594 |
( '2008-01-06 02', 'alter', 'ALTER is_hidden SET NOT NULL', ), |
|---|
| 595 |
( '2009-07-30 00', 'alter', 'ADD author_ip varchar(39) NULL', ), |
|---|
| 596 |
) |
|---|
| 597 |
|
|---|
| 598 |
def is_sticky(self): |
|---|
| 599 |
return self.status & POST_STATUS_STICKY |
|---|
| 600 |
def is_closed(self): |
|---|
| 601 |
return self.status & POST_STATUS_CLOSED |
|---|
| 602 |
def is_poll(self): |
|---|
| 603 |
return self.status & POST_STATUS_POLL |
|---|
| 604 |
def is_annotated(self): |
|---|
| 605 |
return self.status & POST_STATUS_ANNOTATED |
|---|
| 606 |
def is_new(self): |
|---|
| 607 |
return self.status & POST_STATUS_NEW |
|---|
| 608 |
|
|---|
| 609 |
def set_sticky(self, sticky): |
|---|
| 610 |
if sticky: self.status = self.status | POST_STATUS_STICKY |
|---|
| 611 |
else: self.status = self.status ^ POST_STATUS_STICKY |
|---|
| 612 |
|
|---|
| 613 |
def set_closed(self, closed): |
|---|
| 614 |
if closed: self.status = self.status | POST_STATUS_CLOSED |
|---|
| 615 |
else: self.status = self.status ^ POST_STATUS_CLOSED |
|---|
| 616 |
|
|---|
| 617 |
def set_poll(self, poll): |
|---|
| 618 |
if poll: self.status = self.status | POST_STATUS_POLL |
|---|
| 619 |
else: self.status = self.status ^ POST_STATUS_POLL |
|---|
| 620 |
|
|---|
| 621 |
def set_annotated(self, annotated): |
|---|
| 622 |
if annotated: self.status = self.status | POST_STATUS_ANNOTATED |
|---|
| 623 |
else: self.status = self.status ^ POST_STATUS_ANNOTATED |
|---|
| 624 |
|
|---|
| 625 |
def set_new(self, new): |
|---|
| 626 |
if new: self.status = self.status | POST_STATUS_NEW |
|---|
| 627 |
else: self.status = self.status ^ POST_STATUS_NEW |
|---|
| 628 |
|
|---|
| 629 |
def get_thread(self): |
|---|
| 630 |
if self.thread == None: return self; |
|---|
| 631 |
return self.thread; |
|---|
| 632 |
|
|---|
| 633 |
def get_threadinformation(self): |
|---|
| 634 |
return ThreadInformation.objects.type_default().get( root_post = self.get_thread() ) |
|---|
| 635 |
|
|---|
| 636 |
def get_latest_post(self): |
|---|
| 637 |
return self.get_all_posts().latest( 'postdate' ) |
|---|
| 638 |
|
|---|
| 639 |
def get_all_posts(self): |
|---|
| 640 |
return Post.objects.filter( Q( pk = self.id ) | Q( thread = self ) ) |
|---|
| 641 |
|
|---|
| 642 |
def replies(self): |
|---|
| 643 |
return Post.objects.filter( thread = self ) |
|---|
| 644 |
|
|---|
| 645 |
def postCount(self): |
|---|
| 646 |
return self.get_all_posts().count() |
|---|
| 647 |
|
|---|
| 648 |
def replyCount(self): |
|---|
| 649 |
return self.replies().count() |
|---|
| 650 |
|
|---|
| 651 |
def allow_posting(self, user): |
|---|
| 652 |
""" |
|---|
| 653 |
Returns True if the user is allowed to post replies in this thread. |
|---|
| 654 |
|
|---|
| 655 |
if user is None, the current user is taken into account. |
|---|
| 656 |
""" |
|---|
| 657 |
return not self.is_closed() and \ |
|---|
| 658 |
( self.category.testAllowance( user, self.category.allowreplies ) \ |
|---|
| 659 |
or has_permission_flag( user, 'sphboard_post_replies', self.category ) ) |
|---|
| 660 |
allowPosting = allow_posting |
|---|
| 661 |
|
|---|
| 662 |
def allow_editing(self, user = None): |
|---|
| 663 |
""" |
|---|
| 664 |
Returns True if the user is allowed to edit this post. |
|---|
| 665 |
|
|---|
| 666 |
if user is None, the current user is taken into account. |
|---|
| 667 |
""" |
|---|
| 668 |
if user == None: user = get_current_user() |
|---|
| 669 |
|
|---|
| 670 |
if not user or not user.is_authenticated(): |
|---|
| 671 |
|
|---|
| 672 |
if self.is_hidden != 0 and self.is_new(): |
|---|
| 673 |
return True |
|---|
| 674 |
return False |
|---|
| 675 |
|
|---|
| 676 |
if user.is_superuser \ |
|---|
| 677 |
or has_permission_flag( user, 'sphboard_editallposts', self.category ): |
|---|
| 678 |
return True |
|---|
| 679 |
|
|---|
| 680 |
if user == self.author: |
|---|
| 681 |
|
|---|
| 682 |
remaining = self.remaining_edit_seconds(user) |
|---|
| 683 |
if remaining == -1 or remaining > 0: |
|---|
| 684 |
return True |
|---|
| 685 |
|
|---|
| 686 |
return False |
|---|
| 687 |
|
|---|
| 688 |
def remaining_edit_seconds(self, user = None): |
|---|
| 689 |
""" |
|---|
| 690 |
Returns the number of seconds the user is allowed to edit the post |
|---|
| 691 |
returns -1 for unlimited (Not checking user permissions !!) |
|---|
| 692 |
""" |
|---|
| 693 |
if user is None: user = get_current_user() |
|---|
| 694 |
|
|---|
| 695 |
timeout = get_sph_setting( 'board_edit_timeout' ) |
|---|
| 696 |
|
|---|
| 697 |
if timeout < 0: |
|---|
| 698 |
return timeout |
|---|
| 699 |
|
|---|
| 700 |
delta = (datetime.today() - self.postdate) |
|---|
| 701 |
totalseconds = delta.days * 24 * 60 * 60 + delta.seconds |
|---|
| 702 |
|
|---|
| 703 |
if timeout >= totalseconds: |
|---|
| 704 |
return timeout - totalseconds |
|---|
| 705 |
|
|---|
| 706 |
|
|---|
| 707 |
return 0 |
|---|
| 708 |
|
|---|
| 709 |
allowEditing = allow_editing |
|---|
| 710 |
|
|---|
| 711 |
def _allow_adminfunctionality(self, flag, user = None): |
|---|
| 712 |
if self.is_private(): |
|---|
| 713 |
return False |
|---|
| 714 |
|
|---|
| 715 |
if user == None: |
|---|
| 716 |
user = get_current_user() |
|---|
| 717 |
|
|---|
| 718 |
if not user or not user.is_authenticated(): |
|---|
| 719 |
return False |
|---|
| 720 |
|
|---|
| 721 |
return user.is_staff or has_permission_flag( user, flag, self.category ) |
|---|
| 722 |
|
|---|
| 723 |
def allow_annotating(self, user = None): |
|---|
| 724 |
""" |
|---|
| 725 |
Returns True if the user is allowed to annotate this post. |
|---|
| 726 |
|
|---|
| 727 |
if user is None, the current user is taken into account. |
|---|
| 728 |
""" |
|---|
| 729 |
return self._allow_adminfunctionality( 'sphboard_annotate', user ) |
|---|
| 730 |
|
|---|
| 731 |
def allow_moving(self, user = None): |
|---|
| 732 |
return self._allow_adminfunctionality( 'sphboard_move', user ) |
|---|
| 733 |
|
|---|
| 734 |
def allow_locking(self, user = None): |
|---|
| 735 |
return self._allow_adminfunctionality( 'sphboard_lock', user ) |
|---|
| 736 |
|
|---|
| 737 |
def allow_sticking(self, user = None): |
|---|
| 738 |
return self._allow_adminfunctionality( 'sphboard_sticky', user ) |
|---|
| 739 |
|
|---|
| 740 |
def has_view_permission(self, user = None): |
|---|
| 741 |
if not user: |
|---|
| 742 |
user = get_current_user() |
|---|
| 743 |
|
|---|
| 744 |
if self.is_private(): |
|---|
| 745 |
if self.author_id == user.id: |
|---|
| 746 |
return True |
|---|
| 747 |
|
|---|
| 748 |
for rec in self.recipient_set.all(): |
|---|
| 749 |
if rec.user_id == user.id: |
|---|
| 750 |
return True |
|---|
| 751 |
|
|---|
| 752 |
return False |
|---|
| 753 |
|
|---|
| 754 |
else: |
|---|
| 755 |
return self.category.has_view_permission(user) |
|---|
| 756 |
|
|---|
| 757 |
def is_private(self): |
|---|
| 758 |
return self.category.is_private() |
|---|
| 759 |
|
|---|
| 760 |
def __get_render_cachekey(self): |
|---|
| 761 |
return 'sphboard_rendered_body_%s' % str(self.id) |
|---|
| 762 |
|
|---|
| 763 |
def body_escaped(self, with_signature = True): |
|---|
| 764 |
""" returns the rendered body. """ |
|---|
| 765 |
body = self.body |
|---|
| 766 |
markup = self.markup |
|---|
| 767 |
if not markup: |
|---|
| 768 |
markup = POST_MARKUP_CHOICES[0][0] |
|---|
| 769 |
|
|---|
| 770 |
|
|---|
| 771 |
bodyhtml = None |
|---|
| 772 |
cachekey = None |
|---|
| 773 |
if self.id: |
|---|
| 774 |
cachekey = self.__get_render_cachekey() |
|---|
| 775 |
bodyhtml = cache.get( cachekey ) |
|---|
| 776 |
if bodyhtml is None: |
|---|
| 777 |
|
|---|
| 778 |
bodyhtml = render_body( body, markup ) |
|---|
| 779 |
if cachekey is not None: |
|---|
| 780 |
cache.set( cachekey, bodyhtml, get_sph_setting( 'board_body_cache_timeout' ) ) |
|---|
| 781 |
|
|---|
| 782 |
if self.author_id and with_signature: |
|---|
| 783 |
signature = get_rendered_signature( self.author_id ) |
|---|
| 784 |
if signature: |
|---|
| 785 |
bodyhtml += '<div class="signature">%s</div>' % signature |
|---|
| 786 |
return mark_safe(bodyhtml) |
|---|
| 787 |
|
|---|
| 788 |
def body_rendered_without_signature(self): |
|---|
| 789 |
return self.body_escaped(with_signature = False) |
|---|
| 790 |
|
|---|
| 791 |
def clear_render_cache(self): |
|---|
| 792 |
cache.delete( self.__get_render_cachekey() ) |
|---|
| 793 |
|
|---|
| 794 |
def viewed(self, session, user): |
|---|
| 795 |
if get_sph_setting( 'board_count_views' ): |
|---|
| 796 |
threadinfo = self.get_threadinformation() |
|---|
| 797 |
threadinfo.view_count += 1 |
|---|
| 798 |
threadinfo.save() |
|---|
| 799 |
self.touch(session, user) |
|---|
| 800 |
|
|---|
| 801 |
def touch(self, session, user): |
|---|
| 802 |
return self._touch( session, user ) |
|---|
| 803 |
|
|---|
| 804 |
def _touch(self, session, user): |
|---|
| 805 |
if not user.is_authenticated(): return None |
|---|
| 806 |
if not self._hasNewPosts(session, user): return |
|---|
| 807 |
thread = self.thread or self |
|---|
| 808 |
try: |
|---|
| 809 |
threadLastVisit = ThreadLastVisit.objects.filter( user = user, |
|---|
| 810 |
thread = thread, )[0] |
|---|
| 811 |
except IndexError: |
|---|
| 812 |
threadLastVisit = ThreadLastVisit( user = user, |
|---|
| 813 |
thread = thread, ) |
|---|
| 814 |
|
|---|
| 815 |
threadLastVisit.lastvisit = datetime.today() |
|---|
| 816 |
threadLastVisit.save() |
|---|
| 817 |
|
|---|
| 818 |
def has_new_posts(self): |
|---|
| 819 |
if hasattr(self, '__has_new_posts'): return self.__has_new_posts |
|---|
| 820 |
self.__has_new_posts = self._hasNewPosts(get_current_session(), get_current_user()) |
|---|
| 821 |
return self.__has_new_posts |
|---|
| 822 |
|
|---|
| 823 |
def get_latest_post(self): |
|---|
| 824 |
try: |
|---|
| 825 |
latestPost = Post.objects.filter( thread = self.id ).latest( 'postdate' ) |
|---|
| 826 |
except Post.DoesNotExist: |
|---|
| 827 |
|
|---|
| 828 |
latestPost = self |
|---|
| 829 |
return latestPost |
|---|
| 830 |
|
|---|
| 831 |
def _hasNewPosts(self, session, user): |
|---|
| 832 |
if not user.is_authenticated(): return False |
|---|
| 833 |
latestPost = self.get_latest_post() |
|---|
| 834 |
categoryLastVisit = self.category.touch(session, user) |
|---|
| 835 |
if categoryLastVisit > latestPost.postdate: |
|---|
| 836 |
return False |
|---|
| 837 |
|
|---|
| 838 |
try: |
|---|
| 839 |
threadLastVisit = ThreadLastVisit.objects.filter( user = user, |
|---|
| 840 |
thread__id = self.id, )[0] |
|---|
| 841 |
return threadLastVisit.lastvisit < latestPost.postdate |
|---|
| 842 |
except IndexError: |
|---|
| 843 |
return True |
|---|
| 844 |
|
|---|
| 845 |
|
|---|
| 846 |
def poll(self): |
|---|
| 847 |
try: |
|---|
| 848 |
return self.poll_set.get() |
|---|
| 849 |
except Poll.DoesNotExist: |
|---|
| 850 |
return None |
|---|
| 851 |
|
|---|
| 852 |
def has_monitor(self): |
|---|
| 853 |
"""Returns True if there is a monitor for the current user in this |
|---|
| 854 |
thread. (Will also return True if there is a monitor in a category !) |
|---|
| 855 |
To check this, call has_direct_monitor ! |
|---|
| 856 |
""" |
|---|
| 857 |
monitor = self.__get_monitor(get_current_user()) |
|---|
| 858 |
if monitor: return True |
|---|
| 859 |
return False |
|---|
| 860 |
|
|---|
| 861 |
def has_direct_monitor(self): |
|---|
| 862 |
"""Return only True if there is a direct monitor for THIS thread.""" |
|---|
| 863 |
monitor = self.__get_monitor(get_current_user()) |
|---|
| 864 |
thread = self.thread or self |
|---|
| 865 |
return monitor and monitor.thread == thread |
|---|
| 866 |
|
|---|
| 867 |
def __get_monitor(self, user): |
|---|
| 868 |
if hasattr(self, '__monitor'): return self.__monitor |
|---|
| 869 |
thread = self.thread or self |
|---|
| 870 |
try: |
|---|
| 871 |
monitor = Monitor.objects.get( thread = thread, |
|---|
| 872 |
user = user, ) |
|---|
| 873 |
except Monitor.DoesNotExist: |
|---|
| 874 |
monitor = thread.category.has_monitor() |
|---|
| 875 |
self.__monitor = monitor |
|---|
| 876 |
return self.__monitor |
|---|
| 877 |
|
|---|
| 878 |
def toggle_monitor(self): |
|---|
| 879 |
if self.has_direct_monitor(): |
|---|
| 880 |
self.__get_monitor(get_current_user()).delete() |
|---|
| 881 |
if hasattr(self, '__monitor'): delattr(self,'__monitor') |
|---|
| 882 |
else: |
|---|
| 883 |
thread = self.thread or self |
|---|
| 884 |
monitor = Monitor( thread = thread, |
|---|
| 885 |
category = thread.category, |
|---|
| 886 |
group = thread.category.group, |
|---|
| 887 |
user = get_current_user(), ) |
|---|
| 888 |
monitor.save() |
|---|
| 889 |
self.__monitor = monitor |
|---|
| 890 |
return monitor |
|---|
| 891 |
|
|---|
| 892 |
def save(self, force_insert=False, force_update=False, additional_data=None): |
|---|
| 893 |
isnew = not self.id |
|---|
| 894 |
|
|---|
| 895 |
if isnew and self.is_hidden != 0: |
|---|
| 896 |
self.set_new( True ) |
|---|
| 897 |
isnew = False |
|---|
| 898 |
elif not isnew and self.is_new() and self.is_hidden == 0: |
|---|
| 899 |
self.set_new( False) |
|---|
| 900 |
isnew = True |
|---|
| 901 |
|
|---|
| 902 |
|
|---|
| 903 |
|
|---|
| 904 |
|
|---|
| 905 |
self.is_new_post = isnew |
|---|
| 906 |
ret = super(Post, self).save(force_insert=force_insert, force_update=force_update) |
|---|
| 907 |
|
|---|
| 908 |
if additional_data is not None: |
|---|
| 909 |
self.category.get_category_type().save_post(self, additional_data) |
|---|
| 910 |
|
|---|
| 911 |
|
|---|
| 912 |
cache.delete( self.__get_render_cachekey() ) |
|---|
| 913 |
if isnew: |
|---|
| 914 |
if not hasattr(settings, 'SPH_SETTINGS') or \ |
|---|
| 915 |
not 'noemailnotifications' in settings.SPH_SETTINGS or \ |
|---|
| 916 |
not settings.SPH_SETTINGS['noemailnotifications']: |
|---|
| 917 |
|
|---|
| 918 |
thread = self.thread or self |
|---|
| 919 |
|
|---|
| 920 |
allmonitors = Monitor.objects.all() |
|---|
| 921 |
monitors = allmonitors.filter( thread = thread ) |
|---|
| 922 |
|
|---|
| 923 |
category = self.category |
|---|
| 924 |
while category: |
|---|
| 925 |
monitors = monitors | allmonitors.filter( category = category, thread__isnull = True ) |
|---|
| 926 |
category = category.parent |
|---|
| 927 |
|
|---|
| 928 |
monitors = monitors | allmonitors.filter( group = self.category.group, category__isnull = True, thread__isnull = True ) |
|---|
| 929 |
|
|---|
| 930 |
|
|---|
| 931 |
subject = '%sNew Forum Post in "%s": %s' % (settings.EMAIL_SUBJECT_PREFIX, self.category.name, self.subject,) |
|---|
| 932 |
group = get_current_group() or self.category.group |
|---|
| 933 |
t = loader.get_template('sphene/sphboard/new_post_email.txt') |
|---|
| 934 |
c = { |
|---|
| 935 |
'baseurl': group.baseurl, |
|---|
| 936 |
'group': group, |
|---|
| 937 |
'post': self, |
|---|
| 938 |
} |
|---|
| 939 |
body = t.render(RequestContext(get_current_request(), c)) |
|---|
| 940 |
|
|---|
| 941 |
|
|---|
| 942 |
datatuple = () |
|---|
| 943 |
sent_email_addresses = () |
|---|
| 944 |
if self.author != None: |
|---|
| 945 |
sent_email_addresses += (self.author.email,) |
|---|
| 946 |
logger.debug('Finding email notification monitors ..') |
|---|
| 947 |
for monitor in monitors: |
|---|
| 948 |
if monitor.user.email in sent_email_addresses : continue |
|---|
| 949 |
if monitor.user.email == '': continue |
|---|
| 950 |
|
|---|
| 951 |
|
|---|
| 952 |
if not self.category.has_view_permission( monitor.user ): |
|---|
| 953 |
logger.info( "User {%s} has monitor but no view permission for category {%s}" % (str(monitor.user), |
|---|
| 954 |
str(self.category),)) |
|---|
| 955 |
continue |
|---|
| 956 |
|
|---|
| 957 |
logger.info( "Adding user {%s} email address to notification email." % str(monitor.user) ) |
|---|
| 958 |
|
|---|
| 959 |
|
|---|
| 960 |
datatuple += (subject, body, None, [monitor.user.email,]), |
|---|
| 961 |
sent_email_addresses += monitor.user.email, |
|---|
| 962 |
|
|---|
| 963 |
logger.info( "Sending email notifications - {%s}" % str(datatuple) ) |
|---|
| 964 |
if datatuple: |
|---|
| 965 |
send_mass_mail(datatuple, ) |
|---|
| 966 |
|
|---|
| 967 |
return ret |
|---|
| 968 |
|
|---|
| 969 |
def __unicode__(self): |
|---|
| 970 |
return self.subject |
|---|
| 971 |
|
|---|
| 972 |
def get_page(self): |
|---|
| 973 |
if self.thread is None: |
|---|
| 974 |
return 1 |
|---|
| 975 |
threadinfo = self.get_threadinformation() |
|---|
| 976 |
if threadinfo.latest_post == self: |
|---|
| 977 |
return threadinfo.get_page_count() |
|---|
| 978 |
|
|---|
| 979 |
i = 0 |
|---|
| 980 |
for post in self.thread.get_all_posts(): |
|---|
| 981 |
i+=1 |
|---|
| 982 |
if post == self: |
|---|
| 983 |
break |
|---|
| 984 |
import math |
|---|
| 985 |
return int(math.ceil(i / float(get_sph_setting( 'board_post_paging' )))) |
|---|
| 986 |
|
|---|
| 987 |
def get_absolute_url(self): |
|---|
| 988 |
cturl = self.category.get_category_type().get_absolute_url_for_post( self ) |
|---|
| 989 |
if cturl: |
|---|
| 990 |
return cturl |
|---|
| 991 |
return "%s?page=%d#post-%d" % (self._get_absolute_url(), |
|---|
| 992 |
self.get_page(), |
|---|
| 993 |
self.id) |
|---|
| 994 |
|
|---|
| 995 |
def _get_absolute_url(self): |
|---|
| 996 |
kwargs = { 'groupName': self.category.group.name, |
|---|
| 997 |
'thread_id': self.thread and self.thread.id or self.id } |
|---|
| 998 |
if get_sph_setting('board_slugify_links'): |
|---|
| 999 |
name = 'sphboard_show_thread' |
|---|
| 1000 |
kwargs['slug'] = slugify(self.get_thread().subject) or '_' |
|---|
| 1001 |
else: |
|---|
| 1002 |
name = 'sphboard_show_thread_without_slug' |
|---|
| 1003 |
return (name, (), kwargs) |
|---|
| 1004 |
_get_absolute_url = sphpermalink(_get_absolute_url) |
|---|
| 1005 |
|
|---|
| 1006 |
def get_absolute_editurl(self): |
|---|
| 1007 |
return ('sphene.sphboard.views.post', (), { 'groupName': self.category.group.name, 'category_id': self.category.id, 'post_id': self.id }) |
|---|
| 1008 |
get_absolute_editurl = sphpermalink(get_absolute_editurl) |
|---|
| 1009 |
|
|---|
| 1010 |
def get_absolute_postreplyurl(self): |
|---|
| 1011 |
return ('sphene.sphboard.views.reply', (), { 'groupName': self.category.group.name, 'category_id': self.category.id, 'thread_id': self.get_thread().id }) |
|---|
| 1012 |
get_absolute_postreplyurl = sphpermalink(get_absolute_postreplyurl) |
|---|
| 1013 |
|
|---|
| 1014 |
def get_absolute_annotate_url(self): |
|---|
| 1015 |
return ('sphene.sphboard.views.annotate', (), { 'groupName': self.category.group.name, 'post_id': self.id }) |
|---|
| 1016 |
get_absolute_annotate_url = sphpermalink(get_absolute_annotate_url) |
|---|
| 1017 |
|
|---|
| 1018 |
|
|---|
| 1019 |
class PostAttachment(models.Model): |
|---|
| 1020 |
post = models.ForeignKey(Post, related_name = 'attachments') |
|---|
| 1021 |
|
|---|
| 1022 |
fileupload = models.FileField( _(u'File'), |
|---|
| 1023 |
upload_to = get_sph_setting( 'board_attachments_upload_to' ), |
|---|
| 1024 |
blank = True ) |
|---|
| 1025 |
|
|---|
| 1026 |
|
|---|
| 1027 |
class PostAnnotation(models.Model): |
|---|
| 1028 |
""" |
|---|
| 1029 |
Represents an admin annotation to a post - for example to hide |
|---|
| 1030 |
a post if it violates the rules. |
|---|
| 1031 |
It is also used as comment field when a thread is moved into |
|---|
| 1032 |
another category. |
|---|
| 1033 |
""" |
|---|
| 1034 |
|
|---|
| 1035 |
|
|---|
| 1036 |
post = models.ForeignKey(Post, related_name = 'annotation', unique = True, ) |
|---|
| 1037 |
body = models.TextField() |
|---|
| 1038 |
author = models.ForeignKey(User) |
|---|
| 1039 |
created = models.DateTimeField( ) |
|---|
| 1040 |
hide_post = models.BooleanField() |
|---|
| 1041 |
markup = models.CharField(max_length = 250, |
|---|
| 1042 |
null = True, |
|---|
| 1043 |
choices = POST_MARKUP_CHOICES, ) |
|---|
| 1044 |
|
|---|
| 1045 |
def save(self, force_insert=False, force_update=False): |
|---|
| 1046 |
if not self.post.is_annotated(): |
|---|
| 1047 |
self.post.set_annotated(True) |
|---|
| 1048 |
self.post.save() |
|---|
| 1049 |
if not self.created: |
|---|
| 1050 |
self.created = datetime.today() |
|---|
| 1051 |
if not self.id: |
|---|
| 1052 |
self.author = get_current_user() |
|---|
| 1053 |
return super(PostAnnotation, self).save(force_insert=force_insert, force_update=force_update) |
|---|
| 1054 |
|
|---|
| 1055 |
def body_escaped(self): |
|---|
| 1056 |
body = self.body |
|---|
| 1057 |
markup = self.markup |
|---|
| 1058 |
if not markup: |
|---|
| 1059 |
markup = POST_MARKUP_CHOICES[0][0] |
|---|
| 1060 |
return mark_safe( render_body( body, markup ) ) |
|---|
| 1061 |
|
|---|
| 1062 |
THREAD_TYPE_DEFAULT = 1 |
|---|
| 1063 |
THREAD_TYPE_MOVED = 2 |
|---|
| 1064 |
|
|---|
| 1065 |
thread_types = ( |
|---|
| 1066 |
(THREAD_TYPE_DEFAULT, 'Default'), |
|---|
| 1067 |
(THREAD_TYPE_MOVED , 'Moved Thread'), |
|---|
| 1068 |
) |
|---|
| 1069 |
|
|---|
| 1070 |
|
|---|
| 1071 |
class ThreadInformationManager(models.Manager): |
|---|
| 1072 |
def type_default(self): |
|---|
| 1073 |
return self.filter( thread_type = THREAD_TYPE_DEFAULT ) |
|---|
| 1074 |
|
|---|
| 1075 |
|
|---|
| 1076 |
class ThreadInformation(models.Model): |
|---|
| 1077 |
""" A object which holds information about threads and caches |
|---|
| 1078 |
a couple of things which are redundant. """ |
|---|
| 1079 |
root_post = models.ForeignKey( Post, null = False, blank = False ) |
|---|
| 1080 |
category = models.ForeignKey( Category ) |
|---|
| 1081 |
|
|---|
| 1082 |
|
|---|
| 1083 |
thread_type = models.IntegerField( choices = thread_types ) |
|---|
| 1084 |
|
|---|
| 1085 |
|
|---|
| 1086 |
heat = models.IntegerField( default = 0, db_index = True ) |
|---|
| 1087 |
heat_calculated = models.DateTimeField( null = True ) |
|---|
| 1088 |
|
|---|
| 1089 |
|
|---|
| 1090 |
sticky_value = models.IntegerField( default = 0, db_index = True ) |
|---|
| 1091 |
latest_post = models.ForeignKey( Post, related_name = 'thread_latest_set' ) |
|---|
| 1092 |
post_count = models.IntegerField( default = 0 ) |
|---|
| 1093 |
view_count = models.IntegerField( default = 0 ) |
|---|
| 1094 |
|
|---|
| 1095 |
thread_latest_postdate = models.DateTimeField( db_index = True ) |
|---|
| 1096 |
|
|---|
| 1097 |
objects = ThreadInformationManager() |
|---|
| 1098 |
|
|---|
| 1099 |
|
|---|
| 1100 |
def save(self, force_insert=False, force_update=False): |
|---|
| 1101 |
if self.thread_latest_postdate is None: |
|---|
| 1102 |
self.thread_latest_postdate = self.latest_post.postdate |
|---|
| 1103 |
|
|---|
| 1104 |
super(ThreadInformation, self).save(force_insert=force_insert, force_update=force_update) |
|---|
| 1105 |
|
|---|
| 1106 |
def is_hot(self): |
|---|
| 1107 |
if self.heat_calculated and (datetime.today() - self.heat_calculated).days > 7: |
|---|
| 1108 |
logger.debug( 'Heat was not calculated in the last 7 days - recalculating...' ) |
|---|
| 1109 |
self.update_heat() |
|---|
| 1110 |
self.save() |
|---|
| 1111 |
|
|---|
| 1112 |
""" Returns True if this thread represents a "Hot" topic. |
|---|
| 1113 |
If it returns True you can look for the 'heat' property for the exact value. """ |
|---|
| 1114 |
return self.heat > 0 |
|---|
| 1115 |
|
|---|
| 1116 |
def update_cache(self): |
|---|
| 1117 |
""" Will update the latest_post and post_count of this model. |
|---|
| 1118 |
(Ie. the cache - or redundant information.) |
|---|
| 1119 |
Does not save this model ! This has to be done by the caller. """ |
|---|
| 1120 |
|
|---|
| 1121 |
self.sticky_value = self.root_post.is_sticky() and 1 or 0 |
|---|
| 1122 |
|
|---|
| 1123 |
self.latest_post = self.root_post.get_latest_post() |
|---|
| 1124 |
self.thread_latest_postdate = self.latest_post.postdate |
|---|
| 1125 |
|
|---|
| 1126 |
self.post_count = self.root_post.postCount() |
|---|
| 1127 |
self.update_heat() |
|---|
| 1128 |
|
|---|
| 1129 |
def update_heat(self): |
|---|
| 1130 |
""" |
|---|
| 1131 |
Updates the heat value - this should be run periodically. |
|---|
| 1132 |
Or at least every time a post is added to a thread. |
|---|
| 1133 |
|
|---|
| 1134 |
The caller has to ensure that the thread is saved afterwards ! |
|---|
| 1135 |
""" |
|---|
| 1136 |
days = get_sph_setting( 'board_heat_days' ) |
|---|
| 1137 |
|
|---|
| 1138 |
|
|---|
| 1139 |
count = self.root_post.get_all_posts().filter( postdate__gte = datetime.today() - timedelta( days ) ).count() |
|---|
| 1140 |
views = self.view_count |
|---|
| 1141 |
|
|---|
| 1142 |
age = -(self.root_post.postdate - datetime.today()).days |
|---|
| 1143 |
|
|---|
| 1144 |
heat_calculator = get_method_by_name( get_sph_setting( 'board_heat_calculator' ) ) |
|---|
| 1145 |
heat = heat_calculator( thread = self, |
|---|
| 1146 |
postcount = count, |
|---|
| 1147 |
viewcount = views, |
|---|
| 1148 |
age = age, ) |
|---|
| 1149 |
logger.debug( "Number of posts in the last %d days: %d - age: %d - views: %d - resulting heat: %d" % (days, count, age, views, heat) ) |
|---|
| 1150 |
self.heat = int(heat) |
|---|
| 1151 |
self.heat_calculated = datetime.today() |
|---|
| 1152 |
|
|---|
| 1153 |
def is_sticky(self): |
|---|
| 1154 |
return self.sticky_value > 0 |
|---|
| 1155 |
|
|---|
| 1156 |
def is_moved(self): |
|---|
| 1157 |
""" Returns true if this thread represents a thread which was moved |
|---|
| 1158 |
into another category. """ |
|---|
| 1159 |
return self.thread_type == THREAD_TYPE_MOVED |
|---|
| 1160 |
|
|---|
| 1161 |
def get_page_count(self): |
|---|
| 1162 |
""" Returns the number of pages this thread has. """ |
|---|
| 1163 |
import math |
|---|
| 1164 |
|
|---|
| 1165 |
return int(math.ceil(self.root_post.postCount() / float(get_sph_setting( 'board_post_paging' )))) |
|---|
| 1166 |
|
|---|
| 1167 |
def has_paging(self): |
|---|
| 1168 |
return self.root_post.postCount() > get_sph_setting( 'board_post_paging' ) |
|---|
| 1169 |
|
|---|
| 1170 |
|
|---|
| 1171 |
|
|---|
| 1172 |
|
|---|
| 1173 |
|
|---|
| 1174 |
def author(self): |
|---|
| 1175 |
return self.root_post.author |
|---|
| 1176 |
|
|---|
| 1177 |
def subject(self): |
|---|
| 1178 |
return self.root_post.subject |
|---|
| 1179 |
|
|---|
| 1180 |
@property |
|---|
| 1181 |
def icon(self): |
|---|
| 1182 |
return self.root_post.icon |
|---|
| 1183 |
|
|---|
| 1184 |
def is_poll(self): |
|---|
| 1185 |
return self.root_post.is_poll() |
|---|
| 1186 |
|
|---|
| 1187 |
def is_closed(self): |
|---|
| 1188 |
return self.root_post.is_closed() |
|---|
| 1189 |
|
|---|
| 1190 |
def has_new_posts(self): |
|---|
| 1191 |
return self.root_post.has_new_posts() |
|---|
| 1192 |
|
|---|
| 1193 |
|
|---|
| 1194 |
|
|---|
| 1195 |
|
|---|
| 1196 |
def get_threadlist_subject(self): |
|---|
| 1197 |
return self.category.get_category_type().get_threadlist_subject( self ) |
|---|
| 1198 |
|
|---|
| 1199 |
def get_absolute_url(self): |
|---|
| 1200 |
cturl = self.category.get_category_type().get_absolute_url_for_post( self.root_post ) |
|---|
| 1201 |
if cturl: |
|---|
| 1202 |
return cturl |
|---|
| 1203 |
|
|---|
| 1204 |
return self.root_post.get_absolute_url() |
|---|
| 1205 |
|
|---|
| 1206 |
def get_absolute_url_nopaging(self): |
|---|
| 1207 |
cturl = self.category.get_category_type().get_absolute_url_for_post( self.root_post ) |
|---|
| 1208 |
if cturl: |
|---|
| 1209 |
return cturl |
|---|
| 1210 |
return self._get_absolute_url() |
|---|
| 1211 |
|
|---|
| 1212 |
def _get_absolute_url(self): |
|---|
| 1213 |
kwargs = { 'groupName': self.category.group.name, |
|---|
| 1214 |
'thread_id': self.root_post.id } |
|---|
| 1215 |
name = 'sphboard_show_thread_without_slug' |
|---|
| 1216 |
if get_sph_setting('board_slugify_links'): |
|---|
| 1217 |
slug = slugify(self.root_post.subject) |
|---|
| 1218 |
if slug: |
|---|
| 1219 |
name = 'sphboard_show_thread' |
|---|
| 1220 |
kwargs['slug'] = slug |
|---|
| 1221 |
return (name, (), kwargs) |
|---|
| 1222 |
_get_absolute_url = sphpermalink(_get_absolute_url) |
|---|
| 1223 |
|
|---|
| 1224 |
def __unicode__(self): |
|---|
| 1225 |
return self.root_post.subject |
|---|
| 1226 |
|
|---|
| 1227 |
|
|---|
| 1228 |
def calculate_heat(thread, postcount, viewcount, age): |
|---|
| 1229 |
""" |
|---|
| 1230 |
This method can be customized by setting board_heat_calculator to your own |
|---|
| 1231 |
method which will replace this one. |
|---|
| 1232 |
|
|---|
| 1233 |
It should return the "heat" (Usually something between -100 and 100 - where >0 should represent |
|---|
| 1234 |
a "hot" thread. |
|---|
| 1235 |
""" |
|---|
| 1236 |
|
|---|
| 1237 |
post_threshold = get_sph_setting( 'board_heat_post_threshold' ) |
|---|
| 1238 |
view_threshold = get_sph_setting( 'board_heat_view_threshold' ) |
|---|
| 1239 |
|
|---|
| 1240 |
|
|---|
| 1241 |
postheat = 0 |
|---|
| 1242 |
viewheat = 0 |
|---|
| 1243 |
if postcount > 0: |
|---|
| 1244 |
postheat = (100. / post_threshold * postcount) |
|---|
| 1245 |
if viewcount > 0: |
|---|
| 1246 |
viewheat = (100. / view_threshold * (float(viewcount)/age)) |
|---|
| 1247 |
|
|---|
| 1248 |
|
|---|
| 1249 |
heat = (postheat + viewheat) - 100 |
|---|
| 1250 |
|
|---|
| 1251 |
return heat |
|---|
| 1252 |
|
|---|
| 1253 |
def update_heat(**kwargs): |
|---|
| 1254 |
""" |
|---|
| 1255 |
This method should be regularly called through a cronjob or similar - |
|---|
| 1256 |
this can be done by simply dispatching the maintenance signal. |
|---|
| 1257 |
|
|---|
| 1258 |
see sphenecoll/sphene/community/signals.py for more details. |
|---|
| 1259 |
""" |
|---|
| 1260 |
all_threads = ThreadInformation.objects.all() |
|---|
| 1261 |
for thread in all_threads: |
|---|
| 1262 |
thread.update_heat() |
|---|
| 1263 |
thread.save() |
|---|
| 1264 |
|
|---|
| 1265 |
sphene.community.signals.maintenance.connect(update_heat) |
|---|
| 1266 |
|
|---|
| 1267 |
def update_thread_information(instance, **kwargs): |
|---|
| 1268 |
""" |
|---|
| 1269 |
Updates the thread information every time a post is saved. |
|---|
| 1270 |
""" |
|---|
| 1271 |
thread = instance.get_thread() |
|---|
| 1272 |
threadinfos = ThreadInformation.objects.filter( root_post = thread ) |
|---|
| 1273 |
|
|---|
| 1274 |
if len(threadinfos) < 1: |
|---|
| 1275 |
if thread.is_hidden != 0: |
|---|
| 1276 |
|
|---|
| 1277 |
|
|---|
| 1278 |
return |
|---|
| 1279 |
threadinfos = ( ThreadInformation( root_post = thread, |
|---|
| 1280 |
category = thread.category, |
|---|
| 1281 |
thread_type = THREAD_TYPE_DEFAULT, ), ) |
|---|
| 1282 |
for threadinfo in threadinfos: |
|---|
| 1283 |
threadinfo.update_cache() |
|---|
| 1284 |
threadinfo.save() |
|---|
| 1285 |
|
|---|
| 1286 |
signals.post_save.connect(update_thread_information, |
|---|
| 1287 |
sender = Post) |
|---|
| 1288 |
|
|---|
| 1289 |
def ensure_thread_information(): |
|---|
| 1290 |
""" |
|---|
| 1291 |
Iterates through all threads and verifies that there is a corresponding |
|---|
| 1292 |
ThreadInformation object. (Useful for updates) |
|---|
| 1293 |
""" |
|---|
| 1294 |
allthreads = Post.objects.filter( thread__isnull = True ) |
|---|
| 1295 |
print "Validating Thread information ..." |
|---|
| 1296 |
for thread in allthreads: |
|---|
| 1297 |
update_thread_information( thread ) |
|---|
| 1298 |
print "Done." |
|---|
| 1299 |
|
|---|
| 1300 |
|
|---|
| 1301 |
class Monitor(models.Model): |
|---|
| 1302 |
"""Monitors allow user to get notified by email on new posts in a |
|---|
| 1303 |
particular thread, category or a whole board of a group.""" |
|---|
| 1304 |
|
|---|
| 1305 |
thread = models.ForeignKey(Post, null = True, blank = True) |
|---|
| 1306 |
category = models.ForeignKey(Category, null = True, blank = True) |
|---|
| 1307 |
group = models.ForeignKey(Group) |
|---|
| 1308 |
user = models.ForeignKey(User) |
|---|
| 1309 |
|
|---|
| 1310 |
|
|---|
| 1311 |
class Poll(models.Model): |
|---|
| 1312 |
post = models.ForeignKey(Post, editable = False) |
|---|
| 1313 |
question = models.CharField( max_length = 250 ) |
|---|
| 1314 |
choices_per_user = models.IntegerField( ) |
|---|
| 1315 |
|
|---|
| 1316 |
def multiplechoice(self): |
|---|
| 1317 |
return self.choices_per_user != 1 |
|---|
| 1318 |
|
|---|
| 1319 |
def choices(self): |
|---|
| 1320 |
return self.pollchoice_set.all() |
|---|
| 1321 |
|
|---|
| 1322 |
def has_voted(self, user = None): |
|---|
| 1323 |
if not user: user = get_current_user() |
|---|
| 1324 |
if not user.is_authenticated(): return False |
|---|
| 1325 |
return self.pollvoters_set.filter( user = user ).count() > 0 |
|---|
| 1326 |
|
|---|
| 1327 |
def total_voters(self): |
|---|
| 1328 |
from django.db import connection |
|---|
| 1329 |
cursor = connection.cursor() |
|---|
| 1330 |
cursor.execute("SELECT COUNT(DISTINCT user_id) as totalvoters FROM sphboard_pollvoters WHERE poll_id = %s", [self.id]) |
|---|
| 1331 |
row = cursor.fetchone() |
|---|
| 1332 |
return row[0] |
|---|
| 1333 |
|
|---|
| 1334 |
def total_votes(self): |
|---|
| 1335 |
return self.pollvoters_set.count() |
|---|
| 1336 |
|
|---|
| 1337 |
def null_votes(self): |
|---|
| 1338 |
return self.pollvoters_set.filter( choice__isnull = True ).count() |
|---|
| 1339 |
|
|---|
| 1340 |
def allow_editing(self, user = None): |
|---|
| 1341 |
return self.post.allow_editing(user) |
|---|
| 1342 |
|
|---|
| 1343 |
def get_absolute_editurl(self): |
|---|
| 1344 |
return ('sphboard_edit_poll', (), { 'poll_id': self.id, }) |
|---|
| 1345 |
get_absolute_editurl = sphpermalink(get_absolute_editurl) |
|---|
| 1346 |
|
|---|
| 1347 |
|
|---|
| 1348 |
class PollChoice(models.Model): |
|---|
| 1349 |
poll = models.ForeignKey(Poll, editable = False) |
|---|
| 1350 |
choice = models.CharField( max_length = 250 ) |
|---|
| 1351 |
count = models.IntegerField() |
|---|
| 1352 |
sortorder = models.IntegerField( default = 0, null = False ) |
|---|
| 1353 |
|
|---|
| 1354 |
changelog = ( ( '2008-03-14 00', 'alter', 'ADD sortorder INTEGER' ), |
|---|
| 1355 |
( '2008-03-14 01', 'update', 'SET sortorder = 0' ), |
|---|
| 1356 |
( '2008-03-14 02', 'alter', 'ALTER sortorder SET NOT NULL' ), |
|---|
| 1357 |
) |
|---|
| 1358 |
|
|---|
| 1359 |
class Meta: |
|---|
| 1360 |
ordering = [ 'sortorder' ] |
|---|
| 1361 |
|
|---|
| 1362 |
|
|---|
| 1363 |
class PollVoters(models.Model): |
|---|
| 1364 |
poll = models.ForeignKey(Poll, editable = False) |
|---|
| 1365 |
choice = models.ForeignKey(PollChoice, null = True, blank = True, editable = False) |
|---|
| 1366 |
user = models.ForeignKey(User, editable = False) |
|---|
| 1367 |
|
|---|
| 1368 |
|
|---|
| 1369 |
class BoardUserProfile(models.Model): |
|---|
| 1370 |
user = models.ForeignKey( User, unique = True) |
|---|
| 1371 |
signature = models.TextField(default = '') |
|---|
| 1372 |
|
|---|
| 1373 |
markup = models.CharField(max_length = 250, |
|---|
| 1374 |
null = True, |
|---|
| 1375 |
choices = POST_MARKUP_CHOICES, ) |
|---|
| 1376 |
|
|---|
| 1377 |
default_notifyme_value = models.NullBooleanField(null = True, ) |
|---|
| 1378 |
|
|---|
| 1379 |
def render_signature(self): |
|---|
| 1380 |
if self.signature == '': |
|---|
| 1381 |
return '' |
|---|
| 1382 |
return render_body(self.signature, self.markup) |
|---|
| 1383 |
|
|---|
| 1384 |
|
|---|
| 1385 |
class UserPostCountManager(models.Manager): |
|---|
| 1386 |
def get_post_count(self, user, group): |
|---|
| 1387 |
if user is None: |
|---|
| 1388 |
return None |
|---|
| 1389 |
try: |
|---|
| 1390 |
return self.get(user = user, group = group ).post_count |
|---|
| 1391 |
except UserPostCount.DoesNotExist: |
|---|
| 1392 |
return self.update_post_count(user, group) |
|---|
| 1393 |
|
|---|
| 1394 |
def update_post_count(self, user, group): |
|---|
| 1395 |
if user is None: |
|---|
| 1396 |
return None |
|---|
| 1397 |
try: |
|---|
| 1398 |
upc = self.get(user = user, group = group) |
|---|
| 1399 |
|
|---|
| 1400 |
except UserPostCount.DoesNotExist: |
|---|
| 1401 |
upc = UserPostCount(user = user, group = group) |
|---|
| 1402 |
upc.update_post_count() |
|---|
| 1403 |
upc.save() |
|---|
| 1404 |
return upc.post_count |
|---|
| 1405 |
|
|---|
| 1406 |
|
|---|
| 1407 |
class UserPostCount(models.Model): |
|---|
| 1408 |
user = models.ForeignKey( User ) |
|---|
| 1409 |
group = models.ForeignKey( Group, null=True ) |
|---|
| 1410 |
post_count = models.IntegerField() |
|---|
| 1411 |
|
|---|
| 1412 |
objects = UserPostCountManager() |
|---|
| 1413 |
|
|---|
| 1414 |
def update_post_count(self): |
|---|
| 1415 |
qry = self.user.sphboard_post_author_set |
|---|
| 1416 |
try: |
|---|
| 1417 |
qry = qry.filter(category__group = self.group) |
|---|
| 1418 |
except: |
|---|
| 1419 |
qry = qry.filter(category__group__isnull = True).count() |
|---|
| 1420 |
|
|---|
| 1421 |
self.post_count = qry.count() |
|---|
| 1422 |
|
|---|
| 1423 |
class Meta: |
|---|
| 1424 |
unique_together = ( 'user', 'group' ) |
|---|
| 1425 |
|
|---|
| 1426 |
|
|---|
| 1427 |
def update_post_count(instance, **kwargs): |
|---|
| 1428 |
UserPostCount.objects.update_post_count( instance.author, instance.category.group ) |
|---|
| 1429 |
|
|---|
| 1430 |
signals.post_save.connect(update_post_count, |
|---|
| 1431 |
sender = Post) |
|---|
| 1432 |
|
|---|
| 1433 |
|
|---|
| 1434 |
class ExtendedCategoryConfig(models.Model): |
|---|
| 1435 |
category = models.ForeignKey( Category, unique = True ) |
|---|
| 1436 |
|
|---|
| 1437 |
subject_label = models.CharField( max_length = 250, blank = True ) |
|---|
| 1438 |
body_label = models.CharField( max_length = 250, blank = True ) |
|---|
| 1439 |
body_initial = models.TextField(blank = True) |
|---|
| 1440 |
body_help_text = models.TextField(blank = True) |
|---|
| 1441 |
|
|---|
| 1442 |
post_new_thread_label = models.CharField( max_length = 250, blank = True) |
|---|
| 1443 |
above_thread_list_block = models.TextField(blank = True, help_text = 'HTML which will be displayed above the thread list.') |
|---|
| 1444 |
|
|---|
| 1445 |
|
|---|
| 1446 |
def __get_signature_cachekey(user_id): |
|---|
| 1447 |
return 'sphboard_signature_%s' % user_id |
|---|
| 1448 |
|
|---|
| 1449 |
def get_rendered_signature(user_id): |
|---|
| 1450 |
""" Returns the rendered signature for the given user. """ |
|---|
| 1451 |
|
|---|
| 1452 |
cachekey = __get_signature_cachekey(user_id) |
|---|
| 1453 |
rendered_profile = cache.get( cachekey ) |
|---|
| 1454 |
if rendered_profile is not None: |
|---|
| 1455 |
return rendered_profile |
|---|
| 1456 |
|
|---|
| 1457 |
try: |
|---|
| 1458 |
profile = BoardUserProfile.objects.get( user__pk = user_id, ) |
|---|
| 1459 |
|
|---|
| 1460 |
rendered_profile = profile.render_signature() |
|---|
| 1461 |
except BoardUserProfile.DoesNotExist: |
|---|
| 1462 |
rendered_profile = '' |
|---|
| 1463 |
|
|---|
| 1464 |
cache.set( cachekey, rendered_profile, get_sph_setting( 'board_signature_cache_timeout' ) ) |
|---|
| 1465 |
|
|---|
| 1466 |
return rendered_profile |
|---|
| 1467 |
|
|---|
| 1468 |
def clear_signature_cache(instance, **kwargs): |
|---|
| 1469 |
cache.delete( __get_signature_cachekey( instance.user.id ) ) |
|---|
| 1470 |
|
|---|
| 1471 |
|
|---|
| 1472 |
signals.post_save.connect(clear_signature_cache, |
|---|
| 1473 |
sender = BoardUserProfile) |
|---|
| 1474 |
|
|---|
| 1475 |
|
|---|
| 1476 |
def board_profile_edit_init_form(sender, instance, signal, *args, **kwargs): |
|---|
| 1477 |
user = instance.user |
|---|
| 1478 |
try: |
|---|
| 1479 |
profile = BoardUserProfile.objects.get( user = user, ) |
|---|
| 1480 |
except: |
|---|
| 1481 |
profile = BoardUserProfile( user = user ) |
|---|
| 1482 |
|
|---|
| 1483 |
instance.fields['board_settings'] = Separator(label=_(u'Board settings')) |
|---|
| 1484 |
instance.fields['signature'] = forms.CharField(label=_(u'Signature'), |
|---|
| 1485 |
widget = forms.Textarea( attrs = { 'rows': 3, 'cols': 40 } ), |
|---|
| 1486 |
required = False, |
|---|
| 1487 |
initial = profile.signature, ) |
|---|
| 1488 |
if len( POST_MARKUP_CHOICES ) != 1: |
|---|
| 1489 |
instance.fields['markup'] = forms.CharField(widget = forms.Select( choices = POST_MARKUP_CHOICES, ), |
|---|
| 1490 |
required = False, |
|---|
| 1491 |
initial = profile.markup, ) |
|---|
| 1492 |
instance.fields['default_notifyme_value'] = forms.NullBooleanField( label = _(u'Default Notify Me - Value'), |
|---|
| 1493 |
required = False, |
|---|
| 1494 |
initial = profile.default_notifyme_value, ) |
|---|
| 1495 |
|
|---|
| 1496 |
def board_profile_edit_save_form(sender, instance, signal, request, **kwargs): |
|---|
| 1497 |
user = instance.user |
|---|
| 1498 |
data = instance.cleaned_data |
|---|
| 1499 |
try: |
|---|
| 1500 |
profile = BoardUserProfile.objects.get( user = user, ) |
|---|
| 1501 |
except BoardUserProfile.DoesNotExist: |
|---|
| 1502 |
profile = BoardUserProfile( user = user ) |
|---|
| 1503 |
|
|---|
| 1504 |
profile.signature = data['signature'] |
|---|
| 1505 |
if len( POST_MARKUP_CHOICES ) != 1: |
|---|
| 1506 |
profile.markup = data['markup'] |
|---|
| 1507 |
else: |
|---|
| 1508 |
profile.markup = POST_MARKUP_CHOICES[0][0] |
|---|
| 1509 |
profile.default_notifyme_value = data['default_notifyme_value'] |
|---|
| 1510 |
|
|---|
| 1511 |
profile.save() |
|---|
| 1512 |
request.user.message_set.create( message = _(u"Successfully saved board profile.") ) |
|---|
| 1513 |
|
|---|
| 1514 |
def board_profile_display(sender, signal, request, user, **kwargs): |
|---|
| 1515 |
ret = '<tr><th>%s</th><td>%d</td></tr>' % ( |
|---|
| 1516 |
_('Posts'), UserPostCount.objects.get_post_count(user, get_current_group()), ) |
|---|
| 1517 |
try: |
|---|
| 1518 |
profile = BoardUserProfile.objects.get( user = user, ) |
|---|
| 1519 |
|
|---|
| 1520 |
if profile.signature: |
|---|
| 1521 |
ret += '<tr><th colspan="2">%s</th></tr><tr><td colspan="2">%s</td></tr>' % ( |
|---|
| 1522 |
_('Board Signature'), profile.render_signature(), ) |
|---|
| 1523 |
|
|---|
| 1524 |
except BoardUserProfile.DoesNotExist: |
|---|
| 1525 |
pass |
|---|
| 1526 |
|
|---|
| 1527 |
from sphene.sphboard.views import render_latest_posts_of_user |
|---|
| 1528 |
blocks = '<div>%s</div>' % render_latest_posts_of_user(request, get_current_group(), user) |
|---|
| 1529 |
return { 'additionalprofile': ret, |
|---|
| 1530 |
'block': mark_safe(blocks), } |
|---|
| 1531 |
|
|---|
| 1532 |
profile_edit_init_form.connect(board_profile_edit_init_form, sender = EditProfileForm) |
|---|
| 1533 |
profile_edit_save_form.connect(board_profile_edit_save_form, sender = EditProfileForm) |
|---|
| 1534 |
profile_display.connect(board_profile_display) |
|---|
| 1535 |
|
|---|
| 1536 |
|
|---|
| 1537 |
class PostRecipient(models.Model): |
|---|
| 1538 |
""" |
|---|
| 1539 |
The recipient of a private post. |
|---|
| 1540 |
""" |
|---|
| 1541 |
RECIPIENT_TYPE_CHOICES = ( |
|---|
| 1542 |
(u'TO', _("To")), |
|---|
| 1543 |
(u'CC', _("Cc")), |
|---|
| 1544 |
(u'BCC', _("Bcc")), ) |
|---|
| 1545 |
user = models.ForeignKey( User ) |
|---|
| 1546 |
post = models.ForeignKey( Post, related_name = 'recipient_set' ) |
|---|
| 1547 |
type = models.CharField( max_length = 4, choices = RECIPIENT_TYPE_CHOICES ) |
|---|
| 1548 |
|
|---|
| 1549 |
__labels = dict(RECIPIENT_TYPE_CHOICES) |
|---|
| 1550 |
|
|---|
| 1551 |
def label(self): |
|---|
| 1552 |
return self.__labels.get(self.type, self.type) |
|---|