summaryrefslogtreecommitdiffstats
path: root/askbot/models/tag.py
blob: 455995e0100ce95819fa33a65839787d8aad6ed5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
import re
from django.db import models
from django.contrib.auth.models import User
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy
from django.conf import settings
from askbot.models.base import BaseQuerySetManager
from askbot import const
from askbot.conf import settings as askbot_settings
from askbot.utils import category_tree

def delete_tags(tags):
    """deletes tags in the list"""
    tag_ids = [tag.id for tag in tags]
    Tag.objects.filter(id__in = tag_ids).delete()

def get_tags_by_names(tag_names):
    """returns query set of tags
    and a set of tag names that were not found
    """
    tags = Tag.objects.filter(name__in = tag_names)
    #if there are brand new tags, create them
    #and finalize the added tag list
    if tags.count() < len(tag_names):
        found_tag_names = set([tag.name for tag in tags])
        new_tag_names = set(tag_names) - found_tag_names
    else:
        new_tag_names = set()

    return tags, new_tag_names

def filter_tags_by_status(tags, status = None):
    """returns a list or a query set of tags which are accepted"""
    if isinstance(tags, models.query.QuerySet):
        return tags.filter(status = status)
    else:
        return [tag for tag in tags if tag.status == status]

def filter_accepted_tags(tags):
    return filter_tags_by_status(tags, status = Tag.STATUS_ACCEPTED)

def filter_suggested_tags(tags):
    return filter_tags_by_status(tags, status = Tag.STATUS_SUGGESTED)

def format_personal_group_name(user):
    #todo: after migration of groups away from tags,
    #this function will be moved somewhere else
    from askbot.models.user import PERSONAL_GROUP_NAME_PREFIX as prefix
    return '%s%d' % (prefix, user.id)

def is_preapproved_tag_name(tag_name):
    """true if tag name is in the category tree
    or any other container of preapproved tags"""
    #get list of preapproved tags, to make exceptions for
    if askbot_settings.TAG_SOURCE == 'category-tree':
        return tag_name in category_tree.get_leaf_names()
    return False

def separate_unused_tags(tags):
    """returns two lists::
    * first where tags whose use counts are >0
    * second - with use counts == 0
    """
    used = list()
    unused = list()
    for tag in tags:
        if tag.used_count == 0:
            unused.append(tag)
        else:
            assert(tag.used_count > 0)
            used.append(tag)
    return used, unused

def tags_match_some_wildcard(tag_names, wildcard_tags):
    """Same as
    :meth:`~askbot.models.tag.TagQuerySet.tags_match_some_wildcard`
    except it works on tag name strings
    """
    for tag_name in tag_names:
        for wildcard_tag in sorted(wildcard_tags):
            if tag_name.startswith(wildcard_tag[:-1]):
                return True
    return False

def get_mandatory_tags():
    """returns list of mandatory tags,
    or an empty list, if there aren't any"""
    from askbot.conf import settings as askbot_settings
    #TAG_SOURCE setting is hidden
    #and only is accessible via livesettings overrides
    if askbot_settings.TAG_SOURCE == 'category-tree':
        return []#hack: effectively we disable the mandatory tags feature
    else:
        #todo - in the future clean this up
        #we might need to have settings:
        #* prepopulated tags - json structure - either a flat list or a tree
        #  if structure is tree - then use some multilevel selector for choosing tags
        #  if it is a list - then make users click on tags to select them
        #* use prepopulated tags (boolean)
        #* tags are required
        #* regular users can create tags (boolean)
        #the category tree and the mandatory tag lists can be merged
        #into the same setting - and mandatory tags should use json
        #keep in mind that in the future multiword tags will be allowed
        raw_mandatory_tags = askbot_settings.MANDATORY_TAGS.strip()
        if len(raw_mandatory_tags) == 0:
            return []
        else:
            split_re = re.compile(const.TAG_SPLIT_REGEX)
            return split_re.split(raw_mandatory_tags)


class TagQuerySet(models.query.QuerySet):
    def get_valid_tags(self, page_size):
        tags = self.all().filter(deleted=False).exclude(used_count=0).order_by("-id")[:page_size]
        return tags

    def update_use_counts(self, tags):
        """Updates the given Tags with their current use counts."""
        for tag in tags:
            tag.used_count = tag.threads.count()
            tag.save()

    def mark_undeleted(self):
        """removes deleted(+at/by) marks"""
        self.update(#undelete them
            deleted = False,
            deleted_by = None,
            deleted_at = None
        )

    def tags_match_some_wildcard(self, wildcard_tags = None):
        """True if any one of the tags in the query set
        matches a wildcard

        :arg:`wildcard_tags` is an iterable of wildcard tag strings

        todo: refactor to use :func:`tags_match_some_wildcard`
        """
        for tag in self.all():
            for wildcard_tag in sorted(wildcard_tags):
                if tag.name.startswith(wildcard_tag[:-1]):
                    return True
        return False

    def get_by_wildcards(self, wildcards = None):
        """returns query set of tags that match the wildcard tags
        wildcard tag is guaranteed to end with an asterisk and has
        at least one character preceding the the asterisk. and there
        is only one asterisk in the entire name
        """
        if wildcards is None or len(wildcards) == 0:
            return self.none()
        first_tag = wildcards.pop()
        tag_filter = models.Q(name__startswith = first_tag[:-1])
        for next_tag in wildcards:
            tag_filter |= models.Q(name__startswith = next_tag[:-1])
        return self.filter(tag_filter)

    def get_related_to_search(self, threads, ignored_tag_names):
        """Returns at least tag names, along with use counts"""
        tags = self.filter(threads__in=threads).annotate(local_used_count=models.Count('id')).order_by('-local_used_count', 'name')
        if ignored_tag_names:
            tags = tags.exclude(name__in=ignored_tag_names)
        tags = tags.exclude(deleted = True)
        return list(tags[:50])


class TagManager(BaseQuerySetManager):
    """chainable custom filter query set manager
    for :class:``~askbot.models.Tag`` objects
    """
    def get_query_set(self):
        return TagQuerySet(self.model)

    def get_content_tags(self):
        """temporary function that filters out the group tags"""
        return self.all()

    def create(self, name=None, created_by=None, **kwargs):
        """Creates a new tag"""
        if created_by.can_create_tags() or is_preapproved_tag_name(name):
            status = Tag.STATUS_ACCEPTED
        else:
            status = Tag.STATUS_SUGGESTED

        kwargs['created_by'] = created_by
        kwargs['name'] = name
        kwargs['status'] = status

        return super(TagManager, self).create(**kwargs)

    def create_suggested_tag(self, tag_names = None, user = None):
        """This function is not used, and will probably need
        to be retired. In the previous version we were sending
        email to admins when the new tags were created,
        now we have a separate page where new tags are listed.
        """
        #todo: stuff below will probably go after
        #tag moderation actions are implemented
        from askbot import mail
        from askbot.mail import messages
        body_text = messages.notify_admins_about_new_tags(
                                tags = tag_names,
                                user = user,
                                thread = self
                            )
        site_name = askbot_settings.APP_SHORT_NAME
        subject_line = _('New tags added to %s') % site_name
        mail.mail_moderators(
            subject_line,
            body_text,
            headers = {'Reply-To': user.email}
        )

        msg = _(
            'Tags %s are new and will be submitted for the '
            'moderators approval'
        ) % ', '.join(tag_names)
        user.message_set.create(message = msg)

    def create_in_bulk(self, tag_names = None, user = None):
        """creates tags by names. If user can create tags,
        then they are set status ``STATUS_ACCEPTED``,
        otherwise the status will be set to ``STATUS_SUGGESTED``.

        One exception: if suggested tag is in the category tree
        and source of tags is category tree - then status of newly
        created tag is ``STATUS_ACCEPTED``
        """

        #load suggested tags
        pre_suggested_tags = self.filter(
            name__in = tag_names, status = Tag.STATUS_SUGGESTED
        )

        #deal with suggested tags
        if user.can_create_tags():
            #turn previously suggested tags into accepted
            pre_suggested_tags.update(status = Tag.STATUS_ACCEPTED)
        else:
            #increment use count and add user to "suggested_by"
            for tag in pre_suggested_tags:
                tag.times_used += 1
                tag.suggested_by.add(user)
                tag.save()

        created_tags = list()
        pre_suggested_tag_names = list()
        for tag in pre_suggested_tags:
            pre_suggested_tag_names.append(tag.name)
            created_tags.append(tag)

        for tag_name in set(tag_names) - set(pre_suggested_tag_names):
            #status for the new tags is automatically set within the create()
            new_tag = Tag.objects.create(name = tag_name, created_by = user)
            created_tags.append(new_tag)

            if new_tag.status == Tag.STATUS_SUGGESTED:
                new_tag.suggested_by.add(user)

        return created_tags

def clean_group_name(name):
    """todo: move to the models/user.py
    group names allow spaces,
    tag names do not, so we use this method
    to replace spaces with dashes"""
    return re.sub('\s+', '-', name.strip())

class Tag(models.Model):
    #a couple of status constants
    STATUS_SUGGESTED = 0
    STATUS_ACCEPTED = 1

    name = models.CharField(max_length=255, unique=True)
    created_by = models.ForeignKey(User, related_name='created_tags')

    suggested_by = models.ManyToManyField(
        User, related_name='suggested_tags',
        help_text = 'Works only for suggested tags for tag moderation'
    )

    status = models.SmallIntegerField(default = STATUS_ACCEPTED)

    # Denormalised data
    used_count = models.PositiveIntegerField(default=0)

    deleted     = models.BooleanField(default=False)
    deleted_at  = models.DateTimeField(null=True, blank=True)
    deleted_by  = models.ForeignKey(User, null=True, blank=True, related_name='deleted_tags')

    tag_wiki = models.OneToOneField(
                                'Post',
                                null=True,
                                related_name = 'described_tag'
                            )

    objects = TagManager()

    class Meta:
        app_label = 'askbot'
        db_table = u'tag'
        ordering = ('-used_count', 'name')

    def __unicode__(self):
        return self.name

class MarkedTag(models.Model):
    TAG_MARK_REASONS = (
        ('good', ugettext_lazy('interesting')),
        ('bad', ugettext_lazy('ignored')),
        ('subscribed', ugettext_lazy('subscribed')),
    )
    tag = models.ForeignKey('Tag', related_name='user_selections')
    user = models.ForeignKey(User, related_name='tag_selections')
    reason = models.CharField(max_length=16, choices=TAG_MARK_REASONS)

    class Meta:
        app_label = 'askbot'