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
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
|
import re
from django.db import models
from django.contrib.auth.models import User
from django.utils.translation import get_language
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy
from django.conf import settings as django_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, language_code=None):
"""returns query set of tags
and a set of tag names that were not found
"""
tags = Tag.objects.filter(
name__in=tag_names,
language_code=language_code
)
#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 & models.Q(language_code=get_language()))
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, auto_approve=False, **kwargs):
"""Creates a new tag"""
if auto_approve or 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, language_code=None, auto_approve=False):
"""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``
if `auto_approve` is True then tags are auto-accepted
"""
#load suggested tags
pre_suggested_tags = self.filter(
name__in=tag_names,
status=Tag.STATUS_SUGGESTED,
language_code=language_code
)
#deal with suggested tags
if auto_approve or 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.used_count += 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,
language_code=language_code,
auto_approve=auto_approve
)
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)
created_by = models.ForeignKey(User, related_name='created_tags')
language_code = models.CharField(
choices=django_settings.LANGUAGES,
default=django_settings.LANGUAGE_CODE,
max_length=16,
)
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')
unique_together = ('name', 'language_code')
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'
class TagSynonym(models.Model):
source_tag_name = models.CharField(max_length=255, unique=True)
target_tag_name = models.CharField(max_length=255, db_index=True)
created_at = models.DateTimeField(auto_now_add=True)
owned_by = models.ForeignKey(User, related_name='tag_synonyms')
auto_rename_count = models.IntegerField(default=0)
last_auto_rename_at = models.DateTimeField(auto_now=True)
language_code = models.CharField(
choices=django_settings.LANGUAGES,
default=django_settings.LANGUAGE_CODE,
max_length=16,
)
class Meta:
app_label = 'askbot'
def __unicode__(self):
return u'%s -> %s' % (self.source_tag_name, self.target_tag_name)
|