~widelands-dev/widelands-website/trunk

« back to all changes in this revision

Viewing changes to threadedcomments/models.py

  • Committer: Holger Rapp
  • Date: 2019-06-21 18:34:42 UTC
  • mfrom: (540.1.3 update_ops_script)
  • Revision ID: sirver@gmx.de-20190621183442-y2ulybzr0rdvfefd
Adapt the update script for the new server.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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_text
 
10
 
 
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)
 
14
 
 
15
MARKDOWN = 1
 
16
TEXTILE = 2
 
17
REST = 3
 
18
PLAINTEXT = 5
 
19
MARKUP_CHOICES = (
 
20
    (MARKDOWN, _('markdown')),
 
21
    (TEXTILE, _('textile')),
 
22
    (REST, _('restructuredtext')),
 
23
    (PLAINTEXT, _('plaintext')),
 
24
)
 
25
 
 
26
DEFAULT_MARKUP = getattr(settings, 'DEFAULT_MARKUP', PLAINTEXT)
 
27
 
 
28
 
 
29
def dfs(node, all_nodes, depth):
 
30
    """
 
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.
 
34
    """
 
35
    node.depth = depth
 
36
    to_return = [node, ]
 
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))
 
40
    return to_return
 
41
 
 
42
 
 
43
class ThreadedCommentManager(models.Manager):
 
44
    """A ``Manager`` which will be attached to each comment model.
 
45
 
 
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.
 
49
 
 
50
    """
 
51
 
 
52
    def get_tree(self, content_object, root=None):
 
53
        """
 
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.
 
57
 
 
58
        If root is specified, it will start the tree from that comment's ID.
 
59
 
 
60
        Ideally, one would use this ``depth`` attribute in the display of the comment to
 
61
        offset that comment by some specified length.
 
62
 
 
63
        The following is a (VERY) simple example of how the depth property might be used in a template:
 
64
 
 
65
            {% for comment in comment_tree %}
 
66
                <p style="margin-left: {{ comment.depth }}em">{{ comment.comment }}</p>
 
67
            {% endfor %}
 
68
        """
 
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'))
 
75
        to_return = []
 
76
        if root:
 
77
            if isinstance(root, int):
 
78
                root_id = root
 
79
            else:
 
80
                root_id = root.id
 
81
            to_return = [c for c in children if c.id == root_id]
 
82
            if to_return:
 
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))
 
87
        else:
 
88
            for child in children:
 
89
                if not child.parent:
 
90
                    to_return.extend(dfs(child, children, 0))
 
91
        return to_return
 
92
 
 
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(
 
97
            content_object)
 
98
        kwargs['object_id'] = getattr(
 
99
            content_object, 'pk', getattr(content_object, 'id'))
 
100
        return kwargs
 
101
 
 
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))
 
106
 
 
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))
 
111
 
 
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))
 
115
 
 
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))
 
120
 
 
121
 
 
122
class PublicThreadedCommentManager(ThreadedCommentManager):
 
123
    """
 
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``).
 
127
    """
 
128
 
 
129
    def get_query_set(self):
 
130
        return super(ThreadedCommentManager, self).get_queryset().filter(
 
131
            Q(is_public=True) | Q(is_approved=True)
 
132
        )
 
133
 
 
134
 
 
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``.
 
139
 
 
140
    This ``ThreadedComment`` supports several kinds of markup languages,
 
141
    including Textile, Markdown, and ReST.
 
142
 
 
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``).
 
147
 
 
148
    """
 
149
    # Generic Foreign Key Fields
 
150
    content_type = models.ForeignKey(ContentType)
 
151
    object_id = models.PositiveIntegerField(_('object ID'))
 
152
    content_object = GenericForeignKey()
 
153
 
 
154
    # Hierarchy Field
 
155
    parent = models.ForeignKey(
 
156
        'self', null=True, blank=True, default=None, related_name='children')
 
157
 
 
158
    # User Field
 
159
    user = models.ForeignKey(User)
 
160
 
 
161
    # Date Fields
 
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)
 
168
 
 
169
    # Meat n' Potatoes
 
170
    comment = models.TextField(_('comment'))
 
171
    markup = models.IntegerField(
 
172
        choices=MARKUP_CHOICES, default=DEFAULT_MARKUP, null=True, blank=True)
 
173
 
 
174
    # Status Fields
 
175
    is_public = models.BooleanField(_('is public'), default=True)
 
176
    is_approved = models.BooleanField(_('is approved'), default=False)
 
177
 
 
178
    objects = ThreadedCommentManager()
 
179
    public = PublicThreadedCommentManager()
 
180
 
 
181
    def __str__(self):
 
182
        if len(self.comment) > 50:
 
183
            return self.comment[:50] + '...'
 
184
        return self.comment[:50]
 
185
 
 
186
    def save(self, **kwargs):
 
187
        if not self.markup:
 
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)
 
193
 
 
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
 
198
 
 
199
    class Meta:
 
200
        ordering = ('-date_submitted',)
 
201
        verbose_name = _('Threaded Comment')
 
202
        verbose_name_plural = _('Threaded Comments')
 
203
        get_latest_by = 'date_submitted'