1
from django.db import models
2
from django.contrib.contenttypes.models import ContentType
3
from django.contrib.contenttypes.fields import GenericForeignKey
4
from django.contrib.auth.models import User
5
from datetime import datetime
6
from django.db.models import Q
7
from django.utils.translation import ugettext_lazy as _
8
from django.conf import settings
9
from django.utils.encoding import force_unicode
11
DEFAULT_MAX_COMMENT_LENGTH = getattr(
12
settings, 'DEFAULT_MAX_COMMENT_LENGTH', 1000)
13
DEFAULT_MAX_COMMENT_DEPTH = getattr(settings, 'DEFAULT_MAX_COMMENT_DEPTH', 8)
20
(MARKDOWN, _('markdown')),
21
(TEXTILE, _('textile')),
22
(REST, _('restructuredtext')),
23
(PLAINTEXT, _('plaintext')),
26
DEFAULT_MARKUP = getattr(settings, 'DEFAULT_MARKUP', PLAINTEXT)
29
def dfs(node, all_nodes, depth):
31
Performs a recursive depth-first search starting at ``node``. This function
32
also annotates an attribute, ``depth``, which is an integer that represents
33
how deeply nested this node is away from the original object.
37
for subnode in all_nodes:
38
if subnode.parent and subnode.parent.id == node.id:
39
to_return.extend(dfs(subnode, all_nodes, depth + 1))
43
class ThreadedCommentManager(models.Manager):
44
"""A ``Manager`` which will be attached to each comment model.
46
It helps to facilitate the retrieval of comments in tree form and
47
also has utility methods for creating and retrieving objects related
48
to a specific content object.
52
def get_tree(self, content_object, root=None):
54
Runs a depth-first search on all comments related to the given content_object.
55
This depth-first search adds a ``depth`` attribute to the comment which
56
signifies how how deeply nested the comment is away from the original object.
58
If root is specified, it will start the tree from that comment's ID.
60
Ideally, one would use this ``depth`` attribute in the display of the comment to
61
offset that comment by some specified length.
63
The following is a (VERY) simple example of how the depth property might be used in a template:
65
{% for comment in comment_tree %}
66
<p style="margin-left: {{ comment.depth }}em">{{ comment.comment }}</p>
69
content_type = ContentType.objects.get_for_model(content_object)
70
children = list(self.get_query_set().filter(
71
content_type=content_type,
72
object_id=getattr(content_object, 'pk',
73
getattr(content_object, 'id')),
74
).select_related().order_by('date_submitted'))
77
if isinstance(root, int):
81
to_return = [c for c in children if c.id == root_id]
83
to_return[0].depth = 0
84
for child in children:
85
if child.parent_id == root_id:
86
to_return.extend(dfs(child, children, 1))
88
for child in children:
90
to_return.extend(dfs(child, children, 0))
93
def _generate_object_kwarg_dict(self, content_object, **kwargs):
94
"""Generates the most comment keyword arguments for a given
95
``content_object``."""
96
kwargs['content_type'] = ContentType.objects.get_for_model(
98
kwargs['object_id'] = getattr(
99
content_object, 'pk', getattr(content_object, 'id'))
102
def create_for_object(self, content_object, **kwargs):
103
"""A simple wrapper around ``create`` for a given
104
``content_object``."""
105
return self.create(**self._generate_object_kwarg_dict(content_object, **kwargs))
107
def get_or_create_for_object(self, content_object, **kwargs):
108
"""A simple wrapper around ``get_or_create`` for a given
109
``content_object``."""
110
return self.get_or_create(**self._generate_object_kwarg_dict(content_object, **kwargs))
112
def get_for_object(self, content_object, **kwargs):
113
"""A simple wrapper around ``get`` for a given ``content_object``."""
114
return self.get(**self._generate_object_kwarg_dict(content_object, **kwargs))
116
def all_for_object(self, content_object, **kwargs):
117
"""Prepopulates a QuerySet with all comments related to the given
118
``content_object``."""
119
return self.filter(**self._generate_object_kwarg_dict(content_object, **kwargs))
122
class PublicThreadedCommentManager(ThreadedCommentManager):
124
A ``Manager`` which borrows all of the same methods from ``ThreadedCommentManager``,
125
but which also restricts the queryset to only the published methods
126
(in other words, ``is_public = True``).
129
def get_query_set(self):
130
return super(ThreadedCommentManager, self).get_queryset().filter(
131
Q(is_public=True) | Q(is_approved=True)
135
class ThreadedComment(models.Model):
136
"""A threaded comment which must be associated with an instance of
137
``django.contrib.auth.models.User``. It is given its hierarchy by a
138
nullable relationship back on itself named ``parent``.
140
This ``ThreadedComment`` supports several kinds of markup languages,
141
including Textile, Markdown, and ReST.
143
It also includes two Managers: ``objects``, which is the same as the normal
144
``objects`` Manager with a few added utility functions (see above), and
145
``public``, which has those same utility functions but limits the QuerySet to
146
only those values which are designated as public (``is_public=True``).
149
# Generic Foreign Key Fields
150
content_type = models.ForeignKey(ContentType)
151
object_id = models.PositiveIntegerField(_('object ID'))
152
content_object = GenericForeignKey()
155
parent = models.ForeignKey(
156
'self', null=True, blank=True, default=None, related_name='children')
159
user = models.ForeignKey(User)
162
date_submitted = models.DateTimeField(
163
_('date/time submitted'), default=datetime.now)
164
date_modified = models.DateTimeField(
165
_('date/time modified'), default=datetime.now)
166
date_approved = models.DateTimeField(
167
_('date/time approved'), default=None, null=True, blank=True)
170
comment = models.TextField(_('comment'))
171
markup = models.IntegerField(
172
choices=MARKUP_CHOICES, default=DEFAULT_MARKUP, null=True, blank=True)
175
is_public = models.BooleanField(_('is public'), default=True)
176
is_approved = models.BooleanField(_('is approved'), default=False)
178
objects = ThreadedCommentManager()
179
public = PublicThreadedCommentManager()
181
def __unicode__(self):
182
if len(self.comment) > 50:
183
return self.comment[:50] + '...'
184
return self.comment[:50]
186
def save(self, **kwargs):
188
self.markup = DEFAULT_MARKUP
189
self.date_modified = datetime.now()
190
if not self.date_approved and self.is_approved:
191
self.date_approved = datetime.now()
192
super(ThreadedComment, self).save(**kwargs)
194
def get_content_object(self):
195
"""Wrapper around the GenericForeignKey due to compatibility reasons
196
and due to ``list_display`` limitations."""
197
return self.content_object
200
ordering = ('-date_submitted',)
201
verbose_name = _('Threaded Comment')
202
verbose_name_plural = _('Threaded Comments')
203
get_latest_by = 'date_submitted'