1
from django.db import models
2
from django.contrib.contenttypes.models import ContentType
3
#from django.contrib.contenttypes import generic
4
from django.contrib.contenttypes.fields import GenericForeignKey
5
from django.contrib.auth.models import User
6
from datetime import datetime
7
from django.db.models import Q
8
from django.utils.translation import ugettext_lazy as _
9
from django.conf import settings
10
from django.utils.encoding import force_unicode
12
DEFAULT_MAX_COMMENT_LENGTH = getattr(settings, 'DEFAULT_MAX_COMMENT_LENGTH', 1000)
13
DEFAULT_MAX_COMMENT_DEPTH = getattr(settings, 'DEFAULT_MAX_COMMENT_DEPTH', 8)
21
(MARKDOWN, _("markdown")),
22
(TEXTILE, _("textile")),
23
(REST, _("restructuredtext")),
25
(PLAINTEXT, _("plaintext")),
28
DEFAULT_MARKUP = getattr(settings, 'DEFAULT_MARKUP', PLAINTEXT)
30
def dfs(node, all_nodes, depth):
32
Performs a recursive depth-first search starting at ``node``. This function
33
also annotates an attribute, ``depth``, which is an integer that represents
34
how deeply nested this node is away from the original object.
38
for subnode in all_nodes:
39
if subnode.parent and subnode.parent.id == node.id:
40
to_return.extend(dfs(subnode, all_nodes, depth+1))
43
class ThreadedCommentManager(models.Manager):
45
A ``Manager`` which will be attached to each comment model. It helps to facilitate
46
the retrieval of comments in tree form and also has utility methods for
47
creating and retrieving objects related to a specific content object.
49
def get_tree(self, content_object, root=None):
51
Runs a depth-first search on all comments related to the given content_object.
52
This depth-first search adds a ``depth`` attribute to the comment which
53
signifies how how deeply nested the comment is away from the original object.
55
If root is specified, it will start the tree from that comment's ID.
57
Ideally, one would use this ``depth`` attribute in the display of the comment to
58
offset that comment by some specified length.
60
The following is a (VERY) simple example of how the depth property might be used in a template:
62
{% for comment in comment_tree %}
63
<p style="margin-left: {{ comment.depth }}em">{{ comment.comment }}</p>
66
content_type = ContentType.objects.get_for_model(content_object)
67
children = list(self.get_query_set().filter(
68
content_type = content_type,
69
object_id = getattr(content_object, 'pk', getattr(content_object, 'id')),
70
).select_related().order_by('date_submitted'))
73
if isinstance(root, int):
77
to_return = [c for c in children if c.id == root_id]
79
to_return[0].depth = 0
80
for child in children:
81
if child.parent_id == root_id:
82
to_return.extend(dfs(child, children, 1))
84
for child in children:
86
to_return.extend(dfs(child, children, 0))
89
def _generate_object_kwarg_dict(self, content_object, **kwargs):
91
Generates the most comment keyword arguments for a given ``content_object``.
93
kwargs['content_type'] = ContentType.objects.get_for_model(content_object)
94
kwargs['object_id'] = getattr(content_object, 'pk', getattr(content_object, 'id'))
97
def create_for_object(self, content_object, **kwargs):
99
A simple wrapper around ``create`` for a given ``content_object``.
101
return self.create(**self._generate_object_kwarg_dict(content_object, **kwargs))
103
def get_or_create_for_object(self, content_object, **kwargs):
105
A simple wrapper around ``get_or_create`` for a given ``content_object``.
107
return self.get_or_create(**self._generate_object_kwarg_dict(content_object, **kwargs))
109
def get_for_object(self, content_object, **kwargs):
111
A simple wrapper around ``get`` for a given ``content_object``.
113
return self.get(**self._generate_object_kwarg_dict(content_object, **kwargs))
115
def all_for_object(self, content_object, **kwargs):
117
Prepopulates a QuerySet with all comments related to the given ``content_object``.
119
return self.filter(**self._generate_object_kwarg_dict(content_object, **kwargs))
121
class PublicThreadedCommentManager(ThreadedCommentManager):
123
A ``Manager`` which borrows all of the same methods from ``ThreadedCommentManager``,
124
but which also restricts the queryset to only the published methods
125
(in other words, ``is_public = True``).
127
def get_query_set(self):
128
return super(ThreadedCommentManager, self).get_queryset().filter(
129
Q(is_public = True) | Q(is_approved = True)
132
class ThreadedComment(models.Model):
134
A threaded comment which must be associated with an instance of
135
``django.contrib.auth.models.User``. It is given its hierarchy by
136
a nullable relationship back on itself named ``parent``.
138
This ``ThreadedComment`` supports several kinds of markup languages,
139
including Textile, Markdown, and ReST.
141
It also includes two Managers: ``objects``, which is the same as the normal
142
``objects`` Manager with a few added utility functions (see above), and
143
``public``, which has those same utility functions but limits the QuerySet to
144
only those values which are designated as public (``is_public=True``).
146
# Generic Foreign Key Fields
147
content_type = models.ForeignKey(ContentType)
148
object_id = models.PositiveIntegerField(_('object ID'))
149
content_object = GenericForeignKey()
152
parent = models.ForeignKey('self', null=True, blank=True, default=None, related_name='children')
155
user = models.ForeignKey(User)
158
date_submitted = models.DateTimeField(_('date/time submitted'), default = datetime.now)
159
date_modified = models.DateTimeField(_('date/time modified'), default = datetime.now)
160
date_approved = models.DateTimeField(_('date/time approved'), default=None, null=True, blank=True)
163
comment = models.TextField(_('comment'))
164
markup = models.IntegerField(choices=MARKUP_CHOICES, default=DEFAULT_MARKUP, null=True, blank=True)
167
is_public = models.BooleanField(_('is public'), default = True)
168
is_approved = models.BooleanField(_('is approved'), default = False)
171
ip_address = models.GenericIPAddressField(_('IP address'), null=True, blank=True)
173
objects = ThreadedCommentManager()
174
public = PublicThreadedCommentManager()
176
def __unicode__(self):
177
if len(self.comment) > 50:
178
return self.comment[:50] + "..."
179
return self.comment[:50]
181
def save(self, **kwargs):
183
self.markup = DEFAULT_MARKUP
184
self.date_modified = datetime.now()
185
if not self.date_approved and self.is_approved:
186
self.date_approved = datetime.now()
187
super(ThreadedComment, self).save(**kwargs)
189
def get_content_object(self):
191
Wrapper around the GenericForeignKey due to compatibility reasons
192
and due to ``list_display`` limitations.
194
return self.content_object
196
def get_base_data(self, show_dates=True):
198
Outputs a Python dictionary representing the most useful bits of
199
information about this particular object instance.
201
This is mostly useful for testing purposes, as the output from the
202
serializer changes from run to run. However, this may end up being
203
useful for JSON and/or XML data exchange going forward and as the
204
serializer system is changed.
207
for markup_choice in MARKUP_CHOICES:
208
if self.markup == markup_choice[0]:
209
markup = markup_choice[1]
212
'content_object' : self.content_object,
213
'parent' : self.parent,
215
'comment' : self.comment,
216
'is_public' : self.is_public,
217
'is_approved' : self.is_approved,
218
'ip_address' : self.ip_address,
219
'markup' : force_unicode(markup),
222
to_return['date_submitted'] = self.date_submitted
223
to_return['date_modified'] = self.date_modified
224
to_return['date_approved'] = self.date_approved
228
ordering = ('-date_submitted',)
229
verbose_name = _("Threaded Comment")
230
verbose_name_plural = _("Threaded Comments")
231
get_latest_by = "date_submitted"
234
class FreeThreadedComment(models.Model):
236
A threaded comment which need not be associated with an instance of
237
``django.contrib.auth.models.User``. Instead, it requires minimally a name,
238
and maximally a name, website, and e-mail address. It is given its hierarchy
239
by a nullable relationship back on itself named ``parent``.
241
This ``FreeThreadedComment`` supports several kinds of markup languages,
242
including Textile, Markdown, and ReST.
244
It also includes two Managers: ``objects``, which is the same as the normal
245
``objects`` Manager with a few added utility functions (see above), and
246
``public``, which has those same utility functions but limits the QuerySet to
247
only those values which are designated as public (``is_public=True``).
249
# Generic Foreign Key Fields
250
content_type = models.ForeignKey(ContentType)
251
object_id = models.PositiveIntegerField(_('object ID'))
252
content_object = GenericForeignKey()
255
parent = models.ForeignKey('self', null = True, blank=True, default = None, related_name='children')
257
# User-Replacement Fields
258
name = models.CharField(_('name'), max_length = 128)
259
website = models.URLField(_('site'), blank = True)
260
email = models.EmailField(_('e-mail address'), blank = True)
263
date_submitted = models.DateTimeField(_('date/time submitted'), default = datetime.now)
264
date_modified = models.DateTimeField(_('date/time modified'), default = datetime.now)
265
date_approved = models.DateTimeField(_('date/time approved'), default=None, null=True, blank=True)
268
comment = models.TextField(_('comment'))
269
markup = models.IntegerField(choices=MARKUP_CHOICES, default=DEFAULT_MARKUP, null=True, blank=True)
272
is_public = models.BooleanField(_('is public'), default = True)
273
is_approved = models.BooleanField(_('is approved'), default = False)
276
ip_address = models.GenericIPAddressField(_('IP address'), null=True, blank=True)
278
objects = ThreadedCommentManager()
279
public = PublicThreadedCommentManager()
281
def __unicode__(self):
282
if len(self.comment) > 50:
283
return self.comment[:50] + "..."
284
return self.comment[:50]
286
def save(self, **kwargs):
288
self.markup = DEFAULT_MARKUP
289
self.date_modified = datetime.now()
290
if not self.date_approved and self.is_approved:
291
self.date_approved = datetime.now()
292
super(FreeThreadedComment, self).save()
294
def get_content_object(self, **kwargs):
296
Wrapper around the GenericForeignKey due to compatibility reasons
297
and due to ``list_display`` limitations.
299
return self.content_object
301
def get_base_data(self, show_dates=True):
303
Outputs a Python dictionary representing the most useful bits of
304
information about this particular object instance.
306
This is mostly useful for testing purposes, as the output from the
307
serializer changes from run to run. However, this may end up being
308
useful for JSON and/or XML data exchange going forward and as the
309
serializer system is changed.
312
for markup_choice in MARKUP_CHOICES:
313
if self.markup == markup_choice[0]:
314
markup = markup_choice[1]
317
'content_object' : self.content_object,
318
'parent' : self.parent,
320
'website' : self.website,
321
'email' : self.email,
322
'comment' : self.comment,
323
'is_public' : self.is_public,
324
'is_approved' : self.is_approved,
325
'ip_address' : self.ip_address,
326
'markup' : force_unicode(markup),
329
to_return['date_submitted'] = self.date_submitted
330
to_return['date_modified'] = self.date_modified
331
to_return['date_approved'] = self.date_approved
335
ordering = ('-date_submitted',)
336
verbose_name = _("Free Threaded Comment")
337
verbose_name_plural = _("Free Threaded Comments")
338
get_latest_by = "date_submitted"
341
class TestModel(models.Model):
343
This model is simply used by this application's test suite as a model to
344
which to attach comments.
346
name = models.CharField(max_length=5)
347
is_public = models.BooleanField(default=True)
348
date = models.DateTimeField(default=datetime.now)