34
36
how deeply nested this node is away from the original object.
38
40
for subnode in all_nodes:
39
41
if subnode.parent and subnode.parent.id == node.id:
40
to_return.extend(dfs(subnode, all_nodes, depth+1))
42
to_return.extend(dfs(subnode, all_nodes, depth + 1))
43
46
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.
47
"""A ``Manager`` which will be attached to each comment model.
49
It helps to facilitate the retrieval of comments in tree form and
50
also has utility methods for creating and retrieving objects related
51
to a specific content object.
49
55
def get_tree(self, content_object, root=None):
51
57
Runs a depth-first search on all comments related to the given content_object.
52
58
This depth-first search adds a ``depth`` attribute to the comment which
53
59
signifies how how deeply nested the comment is away from the original object.
55
61
If root is specified, it will start the tree from that comment's ID.
57
63
Ideally, one would use this ``depth`` attribute in the display of the comment to
58
64
offset that comment by some specified length.
60
66
The following is a (VERY) simple example of how the depth property might be used in a template:
62
68
{% for comment in comment_tree %}
63
69
<p style="margin-left: {{ comment.depth }}em">{{ comment.comment }}</p>
66
72
content_type = ContentType.objects.get_for_model(content_object)
67
73
children = list(self.get_query_set().filter(
68
content_type = content_type,
69
object_id = getattr(content_object, 'pk', getattr(content_object, 'id')),
74
content_type=content_type,
75
object_id=getattr(content_object, 'pk',
76
getattr(content_object, 'id')),
70
77
).select_related().order_by('date_submitted'))
89
96
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
"""Generates the most comment keyword arguments for a given
98
``content_object``."""
99
kwargs['content_type'] = ContentType.objects.get_for_model(
101
kwargs['object_id'] = getattr(
102
content_object, 'pk', getattr(content_object, 'id'))
97
105
def create_for_object(self, content_object, **kwargs):
99
A simple wrapper around ``create`` for a given ``content_object``.
106
"""A simple wrapper around ``create`` for a given
107
``content_object``."""
101
108
return self.create(**self._generate_object_kwarg_dict(content_object, **kwargs))
103
110
def get_or_create_for_object(self, content_object, **kwargs):
105
A simple wrapper around ``get_or_create`` for a given ``content_object``.
111
"""A simple wrapper around ``get_or_create`` for a given
112
``content_object``."""
107
113
return self.get_or_create(**self._generate_object_kwarg_dict(content_object, **kwargs))
109
115
def get_for_object(self, content_object, **kwargs):
111
A simple wrapper around ``get`` for a given ``content_object``.
116
"""A simple wrapper around ``get`` for a given ``content_object``."""
113
117
return self.get(**self._generate_object_kwarg_dict(content_object, **kwargs))
115
119
def all_for_object(self, content_object, **kwargs):
117
Prepopulates a QuerySet with all comments related to the given ``content_object``.
120
"""Prepopulates a QuerySet with all comments related to the given
121
``content_object``."""
119
122
return self.filter(**self._generate_object_kwarg_dict(content_object, **kwargs))
121
125
class PublicThreadedCommentManager(ThreadedCommentManager):
123
127
A ``Manager`` which borrows all of the same methods from ``ThreadedCommentManager``,
124
128
but which also restricts the queryset to only the published methods
125
129
(in other words, ``is_public = True``).
127
132
def get_query_set(self):
128
133
return super(ThreadedCommentManager, self).get_queryset().filter(
129
Q(is_public = True) | Q(is_approved = True)
134
Q(is_public=True) | Q(is_approved=True)
132
138
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``.
139
"""A threaded comment which must be associated with an instance of
140
``django.contrib.auth.models.User``. It is given its hierarchy by a
141
nullable relationship back on itself named ``parent``.
138
143
This ``ThreadedComment`` supports several kinds of markup languages,
139
144
including Textile, Markdown, and ReST.
141
146
It also includes two Managers: ``objects``, which is the same as the normal
142
147
``objects`` Manager with a few added utility functions (see above), and
143
148
``public``, which has those same utility functions but limits the QuerySet to
144
149
only those values which are designated as public (``is_public=True``).
146
152
# Generic Foreign Key Fields
147
153
content_type = models.ForeignKey(ContentType)
148
154
object_id = models.PositiveIntegerField(_('object ID'))
149
155
content_object = GenericForeignKey()
151
157
# Hierarchy Field
152
parent = models.ForeignKey('self', null=True, blank=True, default=None, related_name='children')
158
parent = models.ForeignKey(
159
'self', null=True, blank=True, default=None, related_name='children')
155
162
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)
165
date_submitted = models.DateTimeField(
166
_('date/time submitted'), default=datetime.now)
167
date_modified = models.DateTimeField(
168
_('date/time modified'), default=datetime.now)
169
date_approved = models.DateTimeField(
170
_('date/time approved'), default=None, null=True, blank=True)
162
172
# Meat n' Potatoes
163
173
comment = models.TextField(_('comment'))
164
markup = models.IntegerField(choices=MARKUP_CHOICES, default=DEFAULT_MARKUP, null=True, blank=True)
174
markup = models.IntegerField(
175
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)
178
is_public = models.BooleanField(_('is public'), default=True)
179
is_approved = models.BooleanField(_('is approved'), default=False)
171
ip_address = models.GenericIPAddressField(_('IP address'), null=True, blank=True)
182
ip_address = models.GenericIPAddressField(
183
_('IP address'), null=True, blank=True)
173
185
objects = ThreadedCommentManager()
174
186
public = PublicThreadedCommentManager()
176
188
def __unicode__(self):
177
189
if len(self.comment) > 50:
178
return self.comment[:50] + "..."
190
return self.comment[:50] + '...'
179
191
return self.comment[:50]
181
193
def save(self, **kwargs):
182
194
if not self.markup:
183
195
self.markup = DEFAULT_MARKUP
185
197
if not self.date_approved and self.is_approved:
186
198
self.date_approved = datetime.now()
187
199
super(ThreadedComment, self).save(**kwargs)
189
201
def get_content_object(self):
191
Wrapper around the GenericForeignKey due to compatibility reasons
192
and due to ``list_display`` limitations.
202
"""Wrapper around the GenericForeignKey due to compatibility reasons
203
and due to ``list_display`` limitations."""
194
204
return self.content_object
196
206
def get_base_data(self, show_dates=True):
198
Outputs a Python dictionary representing the most useful bits of
207
"""Outputs a Python dictionary representing the most useful bits of
199
208
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.
210
This is mostly useful for testing purposes, as the output from
211
the serializer changes from run to run. However, this may end
212
up being useful for JSON and/or XML data exchange going forward
213
and as the serializer system is changed.
207
217
for markup_choice in MARKUP_CHOICES:
208
218
if self.markup == markup_choice[0]:
209
219
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
'content_object': self.content_object,
223
'parent': self.parent,
225
'comment': self.comment,
226
'is_public': self.is_public,
227
'is_approved': self.is_approved,
228
'ip_address': self.ip_address,
229
'markup': force_unicode(markup),
222
232
to_return['date_submitted'] = self.date_submitted
223
233
to_return['date_modified'] = self.date_modified
224
234
to_return['date_approved'] = self.date_approved
228
238
ordering = ('-date_submitted',)
229
verbose_name = _("Threaded Comment")
230
verbose_name_plural = _("Threaded Comments")
231
get_latest_by = "date_submitted"
239
verbose_name = _('Threaded Comment')
240
verbose_name_plural = _('Threaded Comments')
241
get_latest_by = 'date_submitted'
234
244
class FreeThreadedComment(models.Model):
236
246
A threaded comment which need not be associated with an instance of
237
247
``django.contrib.auth.models.User``. Instead, it requires minimally a name,
238
248
and maximally a name, website, and e-mail address. It is given its hierarchy
239
249
by a nullable relationship back on itself named ``parent``.
241
251
This ``FreeThreadedComment`` supports several kinds of markup languages,
242
252
including Textile, Markdown, and ReST.
244
254
It also includes two Managers: ``objects``, which is the same as the normal
245
255
``objects`` Manager with a few added utility functions (see above), and
246
256
``public``, which has those same utility functions but limits the QuerySet to
250
260
content_type = models.ForeignKey(ContentType)
251
261
object_id = models.PositiveIntegerField(_('object ID'))
252
262
content_object = GenericForeignKey()
254
264
# Hierarchy Field
255
parent = models.ForeignKey('self', null = True, blank=True, default = None, related_name='children')
265
parent = models.ForeignKey(
266
'self', null=True, blank=True, default=None, related_name='children')
257
268
# 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)
269
name = models.CharField(_('name'), max_length=128)
270
website = models.URLField(_('site'), blank=True)
271
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)
274
date_submitted = models.DateTimeField(
275
_('date/time submitted'), default=datetime.now)
276
date_modified = models.DateTimeField(
277
_('date/time modified'), default=datetime.now)
278
date_approved = models.DateTimeField(
279
_('date/time approved'), default=None, null=True, blank=True)
267
281
# Meat n' Potatoes
268
282
comment = models.TextField(_('comment'))
269
markup = models.IntegerField(choices=MARKUP_CHOICES, default=DEFAULT_MARKUP, null=True, blank=True)
283
markup = models.IntegerField(
284
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)
287
is_public = models.BooleanField(_('is public'), default=True)
288
is_approved = models.BooleanField(_('is approved'), default=False)
276
ip_address = models.GenericIPAddressField(_('IP address'), null=True, blank=True)
291
ip_address = models.GenericIPAddressField(
292
_('IP address'), null=True, blank=True)
278
294
objects = ThreadedCommentManager()
279
295
public = PublicThreadedCommentManager()
281
297
def __unicode__(self):
282
298
if len(self.comment) > 50:
283
return self.comment[:50] + "..."
299
return self.comment[:50] + '...'
284
300
return self.comment[:50]
286
302
def save(self, **kwargs):
287
303
if not self.markup:
288
304
self.markup = DEFAULT_MARKUP
290
306
if not self.date_approved and self.is_approved:
291
307
self.date_approved = datetime.now()
292
308
super(FreeThreadedComment, self).save()
294
310
def get_content_object(self, **kwargs):
296
Wrapper around the GenericForeignKey due to compatibility reasons
297
and due to ``list_display`` limitations.
311
"""Wrapper around the GenericForeignKey due to compatibility reasons
312
and due to ``list_display`` limitations."""
299
313
return self.content_object
301
315
def get_base_data(self, show_dates=True):
303
Outputs a Python dictionary representing the most useful bits of
316
"""Outputs a Python dictionary representing the most useful bits of
304
317
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.
319
This is mostly useful for testing purposes, as the output from
320
the serializer changes from run to run. However, this may end
321
up being useful for JSON and/or XML data exchange going forward
322
and as the serializer system is changed.
312
326
for markup_choice in MARKUP_CHOICES:
313
327
if self.markup == markup_choice[0]:
314
328
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),
331
'content_object': self.content_object,
332
'parent': self.parent,
334
'website': self.website,
336
'comment': self.comment,
337
'is_public': self.is_public,
338
'is_approved': self.is_approved,
339
'ip_address': self.ip_address,
340
'markup': force_unicode(markup),
329
343
to_return['date_submitted'] = self.date_submitted
330
344
to_return['date_modified'] = self.date_modified
331
345
to_return['date_approved'] = self.date_approved
335
349
ordering = ('-date_submitted',)
336
verbose_name = _("Free Threaded Comment")
337
verbose_name_plural = _("Free Threaded Comments")
338
get_latest_by = "date_submitted"
350
verbose_name = _('Free Threaded Comment')
351
verbose_name_plural = _('Free Threaded Comments')
352
get_latest_by = 'date_submitted'
341
355
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.
356
"""This model is simply used by this application's test suite as a model to
357
which to attach comments."""
346
358
name = models.CharField(max_length=5)
347
359
is_public = models.BooleanField(default=True)
348
360
date = models.DateTimeField(default=datetime.now)