root/lib/sct/sphenecoll/sphene/sphboard/models.py

Revision 147:942e3ce3d7d2, 57.4 kB (checked in by Daniele Varrazzo <piro@develer.com>, 1 year ago)

Sphene package updated to r803.

Line 
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             # This is also called during syncdb before tables are
109             # created, so for this case catch all exceptions.
110             # see http://sct.sphene.net/board/thread/898/
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()#models.Manager()
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         #return self.posts.filter( thread__isnull = True )
271         if get_sph_setting( 'workaround_select_related_bug' ):
272             # See http://code.djangoproject.com/ticket/4789
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     # For backward compatibility ...
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         # Check if we were already "touched" ;)
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                     # Only set oldlastvisit if we have new posts.
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         # Retrieve last visit ...
382         try:
383             lastVisit = CategoryLastVisit.objects.get( category = self, user = user )
384         except CategoryLastVisit.DoesNotExist:
385             return False
386
387         # If there was no last visit, we didn't store any last visits for threads.
388         # (Because there was no new threads between 'lastvisit' and 'oldlastvisit'
389         #  so use 'lastvisit')
390         lastvisit = lastVisit.oldlastvisit or lastVisit.lastvisit
391        
392         if lastvisit > latestPost.postdate:
393             return False
394
395         # Check all posts to see if they are new ....
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         # All posts are read .. cool.. we can remove all ThreadLastVisit and adapt CategoryLastVisit
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     # the 'new' status is used in combination with the 'is_hidden'.
550     # the first time a post is saved, the save() method sets this status if 'is_hidden'
551     # is non-0 - the first time the save() method is called with is_hidden = 0 this status
552     # is removed again (this is required to know when the Post is actually 'new' and email
553     # notifications can be sent out).
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     # is_hidden allows basic CMS functionality as well as uploads to posts because
579     # basically you can create a Post object without any influence..
580     # (if something is hidden, it is ALWAYS hidden, not even shown to an administrator.
581     #  a custom category type might change this behavior tough by adding a
582     #  administration interface for hidden posts.)
583     is_hidden = models.IntegerField(default = 0, editable = False, db_index = True )
584
585     # allobjects also contain hidden posts.
586     allobjects = models.Manager()
587     # objects only contains non-hidden posts.
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             # Quick hack to make anonymous uploading of attachments possible
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             # Check edit timeout
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         # Timed out ....
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         # Check cache
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             # Nothing found in cache, render body.
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             # if no post was found, the thread is the latest post ...
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         # set a 'is_new_post' attribute which can be checked by post_save
903         # signal handler if this is really a new post (to send out email notifications
904         # or similar)
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         # Clear cache
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                 # Email Notifications ....
918                 thread = self.thread or self
919                 # thread monitors ..
920                 allmonitors = Monitor.objects.all()
921                 monitors = allmonitors.filter( thread = thread )
922                 # any category monitors
923                 category = self.category
924                 while category:
925                     monitors = monitors | allmonitors.filter( category = category, thread__isnull = True )
926                     category = category.parent
927                     # group monitors
928                     monitors = monitors | allmonitors.filter( group = self.category.group, category__isnull = True, thread__isnull = True )
929                     #monitors = Monitor.objects.filter(myQ)
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                 #body = ("%s just posted in a thread or forum you are monitoring: \n" + \
941                 #        "Visit http://%s/%s") % (group.baseurl, self.author.get_full_name(), self.get_absolute_url())
942                 datatuple = ()
943                 sent_email_addresses = ()
944                 if self.author != None:
945                     sent_email_addresses += (self.author.email,) # Exclude the author of the post
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                     # Check Permissions ...
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                     # Add email address to address tuple ...
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     # This is only blank so the form does not throw errors when it was not entered !
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     # Only one annotation per post allowed !
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     # A thread type allows the decleration of a "Moved" thread.
1083     thread_type = models.IntegerField( choices = thread_types )
1084
1085     # the "heat" value between -100 and 100 where >0 represents a "hot" thread.
1086     heat = models.IntegerField( default = 0, db_index = True )
1087     heat_calculated = models.DateTimeField( null = True )
1088
1089     # For performance reasons store latest posts and various counters here ..
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     # To make it even easier / faster to order by the latest post date..
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         # Find sticky ..
1121         self.sticky_value = self.root_post.is_sticky() and 1 or 0
1122         # Find the last post ...
1123         self.latest_post = self.root_post.get_latest_post()
1124         self.thread_latest_postdate = self.latest_post.postdate
1125         # Calculate post count ...
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         # Get the number of posts of the last x days
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         # No idea why ceil wouldn't return a integer value ..
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     ## Some proxy methods which will simply forward
1172     ## the calls to the root post.
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 # to make thread and post interfaces uniform
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         #return self._get_absolute_url()
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     # It is enough to fulfill one threshold to make a hot thread.
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     # Subtract 100 to have non-hot topic <0
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             # If thread is still hidden, don't bother creating a
1277             # ThreadInformation object.
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     # TODO add caching !
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)
Note: See TracBrowser for help on using the browser.