summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEvgeny Fadeev <evgeny.fadeev@gmail.com>2011-02-28 14:08:00 -0500
committerEvgeny Fadeev <evgeny.fadeev@gmail.com>2011-02-28 14:08:00 -0500
commit04dd70280f2a9bcca38a52717031a2f35aa39910 (patch)
treeafc3b227e67b15a3f6ce864357cf4ad301da039d
parent08bc5fa9271e710c7094bb19e2d9c7aa81aa8e2a (diff)
parent6cd71c8523ea8a24c4e05e079987c4e6883c82a1 (diff)
downloadaskbot-04dd70280f2a9bcca38a52717031a2f35aa39910.tar.gz
askbot-04dd70280f2a9bcca38a52717031a2f35aa39910.tar.bz2
askbot-04dd70280f2a9bcca38a52717031a2f35aa39910.zip
Merge branch 'andrei'
-rw-r--r--askbot/conf/forum_data_rules.py14
-rw-r--r--askbot/deps/django_authopenid/views.py7
-rw-r--r--askbot/forms.py25
-rw-r--r--askbot/management/commands/send_email_alerts.py26
-rw-r--r--askbot/migrations/0037_add_marked_tags_to_user_profile.py315
-rw-r--r--askbot/models/__init__.py136
-rw-r--r--askbot/models/content.py47
-rw-r--r--askbot/models/question.py15
-rw-r--r--askbot/models/tag.py21
-rw-r--r--askbot/skins/default/media/js/live_search.js67
-rw-r--r--askbot/skins/default/media/js/post.js93
-rw-r--r--askbot/skins/default/media/js/tag_selector.js209
-rw-r--r--askbot/skins/default/media/js/utils.js274
-rwxr-xr-xaskbot/skins/default/media/style/style.css11
-rw-r--r--askbot/skins/default/templates/blocks/bottom_scripts.html1
-rw-r--r--askbot/skins/default/templates/macros.html9
-rw-r--r--askbot/skins/default/templates/subscribe_for_tags.html19
-rw-r--r--askbot/tests/db_api_tests.py72
-rw-r--r--askbot/urls.py10
-rw-r--r--askbot/views/commands.py116
20 files changed, 1233 insertions, 254 deletions
diff --git a/askbot/conf/forum_data_rules.py b/askbot/conf/forum_data_rules.py
index 7dfee665..bf38393e 100644
--- a/askbot/conf/forum_data_rules.py
+++ b/askbot/conf/forum_data_rules.py
@@ -59,6 +59,20 @@ settings.register(
)
settings.register(
+ livesettings.BooleanValue(
+ FORUM_DATA_RULES,
+ 'USE_WILDCARD_TAGS',
+ default = False,
+ description = _('Use wildcard tags'),
+ help_text = _(
+ 'Wildcard tags can be used to follow or ignore '
+ 'many tags at once, a valid wildcard tag has a single '
+ 'wildcard at the very end'
+ )
+ )
+)
+
+settings.register(
livesettings.IntegerValue(
FORUM_DATA_RULES,
'MAX_COMMENTS_TO_SHOW',
diff --git a/askbot/deps/django_authopenid/views.py b/askbot/deps/django_authopenid/views.py
index b4bd14fc..f29c5b75 100644
--- a/askbot/deps/django_authopenid/views.py
+++ b/askbot/deps/django_authopenid/views.py
@@ -89,7 +89,12 @@ def login(request,user):
#5) send signal with old session key as argument
logging.debug('logged in user %s with session key %s' % (user.username, session_key))
#todo: move to auth app
- signals.user_logged_in.send(user=user,session_key=session_key,sender=None)
+ signals.user_logged_in.send(
+ request = request,
+ user = user,
+ session_key=session_key,
+ sender=None
+ )
#todo: uncouple this from askbot
def logout(request):
diff --git a/askbot/forms.py b/askbot/forms.py
index 8f23d686..b24e3546 100644
--- a/askbot/forms.py
+++ b/askbot/forms.py
@@ -19,6 +19,31 @@ def cleanup_dict(dictionary, key, empty_value):
if key in dictionary and dictionary[key] == empty_value:
del dictionary[key]
+def clean_marked_tagnames(tagnames):
+ """return two strings - one containing tagnames
+ that are straight names of tags, and the second one
+ containing names of wildcard tags,
+ wildcard tags are those that have an asterisk at the end
+ the function does not verify that the tag names are valid
+ """
+ if askbot_settings.USE_WILDCARD_TAGS == False:
+ return tagnames, list()
+
+ pure_tags = list()
+ wildcards = list()
+ for tagname in tagnames:
+ if tagname == '':
+ continue
+ if tagname.endswith('*'):
+ if tagname.count('*') > 1:
+ continue
+ else:
+ wildcards.append(tagname)
+ else:
+ pure_tags.append(tagname)
+
+ return pure_tags, wildcards
+
def filter_choices(remove_choices = None, from_choices = None):
"""a utility function that will remove choice tuples
usable for the forms.ChoicesField from
diff --git a/askbot/management/commands/send_email_alerts.py b/askbot/management/commands/send_email_alerts.py
index cf4ff16a..4a5b6d6c 100644
--- a/askbot/management/commands/send_email_alerts.py
+++ b/askbot/management/commands/send_email_alerts.py
@@ -251,16 +251,34 @@ class Command(NoArgsCommand):
user_selections__user=user
)
- q_all_A = Q_set_A.exclude( tags__in=ignored_tags )
+ wk = user.ignored_tags.strip().split()
+ ignored_by_wildcards = Tag.objects.get_by_wildcards(wk)
- q_all_B = Q_set_B.exclude( tags__in=ignored_tags )
+ q_all_A = Q_set_A.exclude(
+ tags__in = ignored_tags
+ ).exclude(
+ tags__in = ignored_by_wildcards
+ )
+
+ q_all_B = Q_set_B.exclude(
+ tags__in = ignored_tags
+ ).exclude(
+ tags__in = ignored_by_wildcards
+ )
else:
selected_tags = Tag.objects.filter(
user_selections__reason='good',
user_selections__user=user
)
- q_all_A = Q_set_A.filter( tags__in=selected_tags )
- q_all_B = Q_set_B.filter( tags__in=selected_tags )
+
+ wk = user.interesting_tags.strip().split()
+ selected_by_wildcards = Tag.objects.get_by_wildcards(wk)
+
+ tag_filter = Q(tags__in = list(selected_tags)) \
+ | Q(tags__in = list(selected_by_wildcards))
+
+ q_all_A = Q_set_A.filter( tag_filter )
+ q_all_B = Q_set_B.filter( tag_filter )
q_all_A = q_all_A[:askbot_settings.MAX_ALERTS_PER_EMAIL]
q_all_B = q_all_B[:askbot_settings.MAX_ALERTS_PER_EMAIL]
diff --git a/askbot/migrations/0037_add_marked_tags_to_user_profile.py b/askbot/migrations/0037_add_marked_tags_to_user_profile.py
new file mode 100644
index 00000000..f10f53be
--- /dev/null
+++ b/askbot/migrations/0037_add_marked_tags_to_user_profile.py
@@ -0,0 +1,315 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ try:
+ # Adding field 'User.interesting_tags'
+ db.add_column(u'auth_user', 'interesting_tags', self.gf('django.db.models.fields.TextField')(blank=True, default = ''), keep_default=False)
+ # Adding field 'User.ignored_tags'
+ db.add_column(u'auth_user', 'ignored_tags', self.gf('django.db.models.fields.TextField')(blank=True, default = ''), keep_default=False)
+ except:
+ pass
+
+ def backwards(self, orm):
+
+ # Deleting field 'User.interesting_tags'
+ db.delete_column('auth_user', 'interesting_tags')
+ # Deleting field 'User.ignored_tags'
+ db.delete_column('auth_user', 'ignored_tags')
+
+
+ models = {
+ 'askbot.activity': {
+ 'Meta': {'object_name': 'Activity', 'db_table': "u'activity'"},
+ 'active_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'activity_type': ('django.db.models.fields.SmallIntegerField', [], {}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_auditted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Question']", 'null': 'True'}),
+ 'receiving_users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'received_activity'", 'to': "orm['auth.User']"}),
+ 'recipients': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'incoming_activity'", 'through': "'ActivityAuditStatus'", 'to': "orm['auth.User']"}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'askbot.activityauditstatus': {
+ 'Meta': {'unique_together': "(('user', 'activity'),)", 'object_name': 'ActivityAuditStatus'},
+ 'activity': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Activity']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'status': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'askbot.anonymousanswer': {
+ 'Meta': {'object_name': 'AnonymousAnswer'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ip_addr': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'anonymous_answers'", 'to': "orm['askbot.Question']"}),
+ 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
+ 'summary': ('django.db.models.fields.CharField', [], {'max_length': '180'}),
+ 'text': ('django.db.models.fields.TextField', [], {}),
+ 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'})
+ },
+ 'askbot.anonymousquestion': {
+ 'Meta': {'object_name': 'AnonymousQuestion'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ip_addr': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}),
+ 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
+ 'summary': ('django.db.models.fields.CharField', [], {'max_length': '180'}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125'}),
+ 'text': ('django.db.models.fields.TextField', [], {}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '300'}),
+ 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'})
+ },
+ 'askbot.answer': {
+ 'Meta': {'object_name': 'Answer', 'db_table': "u'answer'"},
+ 'accepted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'accepted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'answers'", 'to': "orm['auth.User']"}),
+ 'comment_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'deleted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'deleted_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'deleted_answers'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'html': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'last_edited_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'last_edited_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'last_edited_answers'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'locked': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'locked_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'locked_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'locked_answers'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'offensive_flag_count': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'answers'", 'to': "orm['askbot.Question']"}),
+ 'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'text': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'vote_down_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'vote_up_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'wikified_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'})
+ },
+ 'askbot.answerrevision': {
+ 'Meta': {'object_name': 'AnswerRevision', 'db_table': "u'answer_revision'"},
+ 'answer': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'revisions'", 'to': "orm['askbot.Answer']"}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'answerrevisions'", 'to': "orm['auth.User']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'revised_at': ('django.db.models.fields.DateTimeField', [], {}),
+ 'revision': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'summary': ('django.db.models.fields.CharField', [], {'max_length': '300', 'blank': 'True'}),
+ 'text': ('django.db.models.fields.TextField', [], {})
+ },
+ 'askbot.award': {
+ 'Meta': {'object_name': 'Award', 'db_table': "u'award'"},
+ 'awarded_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'badge': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'award_badge'", 'to': "orm['askbot.BadgeData']"}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'notified': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'award_user'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.badgedata': {
+ 'Meta': {'object_name': 'BadgeData'},
+ 'awarded_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'awarded_to': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'badges'", 'through': "'Award'", 'to': "orm['auth.User']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50', 'db_index': 'True'})
+ },
+ 'askbot.comment': {
+ 'Meta': {'object_name': 'Comment', 'db_table': "u'comment'"},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'comment': ('django.db.models.fields.CharField', [], {'max_length': '2048'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'html': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '2048'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'comments'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.emailfeedsetting': {
+ 'Meta': {'object_name': 'EmailFeedSetting'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'feed_type': ('django.db.models.fields.CharField', [], {'max_length': '16'}),
+ 'frequency': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '8'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'reported_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+ 'subscriber': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'notification_subscriptions'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.favoritequestion': {
+ 'Meta': {'object_name': 'FavoriteQuestion', 'db_table': "u'favorite_question'"},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Question']"}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'user_favorite_questions'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.markedtag': {
+ 'Meta': {'object_name': 'MarkedTag'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'reason': ('django.db.models.fields.CharField', [], {'max_length': '16'}),
+ 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'user_selections'", 'to': "orm['askbot.Tag']"}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tag_selections'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.question': {
+ 'Meta': {'object_name': 'Question', 'db_table': "u'question'"},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'answer_accepted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'answer_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'questions'", 'to': "orm['auth.User']"}),
+ 'close_reason': ('django.db.models.fields.SmallIntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'closed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'closed_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'closed_questions'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'comment_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'deleted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'deleted_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'deleted_questions'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'favorited_by': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'favorite_questions'", 'through': "'FavoriteQuestion'", 'to': "orm['auth.User']"}),
+ 'favourite_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'followed_by': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'followed_questions'", 'to': "orm['auth.User']"}),
+ 'html': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'last_activity_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_activity_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'last_active_in_questions'", 'to': "orm['auth.User']"}),
+ 'last_edited_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'last_edited_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'last_edited_questions'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'locked': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'locked_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'locked_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'locked_questions'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'offensive_flag_count': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'summary': ('django.db.models.fields.CharField', [], {'max_length': '180'}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125'}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'questions'", 'to': "orm['askbot.Tag']"}),
+ 'text': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '300'}),
+ 'view_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'vote_down_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'vote_up_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'wikified_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'})
+ },
+ 'askbot.questionrevision': {
+ 'Meta': {'object_name': 'QuestionRevision', 'db_table': "u'question_revision'"},
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'questionrevisions'", 'to': "orm['auth.User']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'revisions'", 'to': "orm['askbot.Question']"}),
+ 'revised_at': ('django.db.models.fields.DateTimeField', [], {}),
+ 'revision': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'summary': ('django.db.models.fields.CharField', [], {'max_length': '300', 'blank': 'True'}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125'}),
+ 'text': ('django.db.models.fields.TextField', [], {}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '300'})
+ },
+ 'askbot.questionview': {
+ 'Meta': {'object_name': 'QuestionView'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'viewed'", 'to': "orm['askbot.Question']"}),
+ 'when': ('django.db.models.fields.DateTimeField', [], {}),
+ 'who': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'question_views'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.repute': {
+ 'Meta': {'object_name': 'Repute', 'db_table': "u'repute'"},
+ 'comment': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'negative': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'positive': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Question']", 'null': 'True', 'blank': 'True'}),
+ 'reputation': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'reputation_type': ('django.db.models.fields.SmallIntegerField', [], {}),
+ 'reputed_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'askbot.tag': {
+ 'Meta': {'object_name': 'Tag', 'db_table': "u'tag'"},
+ 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'created_tags'", 'to': "orm['auth.User']"}),
+ 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'deleted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'deleted_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'deleted_tags'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'used_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+ },
+ 'askbot.vote': {
+ 'Meta': {'unique_together': "(('content_type', 'object_id', 'user'),)", 'object_name': 'Vote', 'db_table': "u'vote'"},
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'votes'", 'to': "orm['auth.User']"}),
+ 'vote': ('django.db.models.fields.SmallIntegerField', [], {}),
+ 'voted_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'})
+ },
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}),
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'blank': 'True'}),
+ 'has_custom_avatar': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'hide_ignored_questions': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
+ 'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}),
+ 'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
+ 'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}),
+ 'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}),
+ 'tag_filter_setting': ('django.db.models.fields.CharField', [], {'default': "'ignored'", 'max_length': '16'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}),
+ 'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ }
+ }
+
+ complete_apps = ['askbot']
diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py
index d0b3b9fd..60d0e380 100644
--- a/askbot/models/__init__.py
+++ b/askbot/models/__init__.py
@@ -80,6 +80,9 @@ User.add_to_class('show_country', models.BooleanField(default = False))
User.add_to_class('date_of_birth', models.DateField(null=True, blank=True))
User.add_to_class('about', models.TextField(blank=True))
+#interesting tags and ignored tags are to store wildcard tag selections only
+User.add_to_class('interesting_tags', models.TextField(blank = True))
+User.add_to_class('ignored_tags', models.TextField(blank = True))
User.add_to_class('hide_ignored_questions', models.BooleanField(default=False))
User.add_to_class('tag_filter_setting',
models.CharField(
@@ -164,6 +167,40 @@ def user_get_old_vote_for_post(self, post):
return old_votes[0]
+
+def user_has_affinity_to_question(self, question = None, affinity_type = None):
+ """returns True if number of tag overlap of the user tag
+ selection with the question is 0 and False otherwise
+ affinity_type can be either "like" or "dislike"
+ """
+ if affinity_type == 'like':
+ tag_selection_type = 'good'
+ wildcards = self.interesting_tags.split()
+ elif affinity_type == 'dislike':
+ tag_selection_type = 'bad'
+ wildcards = self.ignored_tags.split()
+ else:
+ raise ValueError('unexpected affinity type %s' % str(affinity_type))
+
+ question_tags = question.tags.all()
+ intersecting_tag_selections = self.tag_selections.filter(
+ tag__in = question_tags,
+ reason = tag_selection_type
+ )
+ #count number of overlapping tags
+ if intersecting_tag_selections.count() > 0:
+ return True
+ elif askbot_settings.USE_WILDCARD_TAGS == False:
+ return False
+
+ #match question tags against wildcards
+ for tag in question_tags:
+ for wildcard in wildcards:
+ if tag.name.startswith(wildcard[:-1]):
+ return True
+ return False
+
+
def user_can_have_strong_url(self):
"""True if user's homepage url can be
followed by the search engine crawlers"""
@@ -798,6 +835,47 @@ def user_post_comment(
)
return comment
+def user_mark_tags(self, tagnames, wildcards, reason = None, action = None):
+ """subscribe for or ignore a list of tags"""
+ cleaned_wildcards = list()
+ if wildcards:
+ cleaned_wildcards = self.update_wildcard_tag_selections(
+ action = action,
+ reason = reason,
+ wildcards = wildcards
+ )
+
+ #below we update normal tag selections
+ marked_ts = MarkedTag.objects.filter(
+ user = self,
+ tag__name__in = tagnames
+ )
+ #todo: use the user api methods here instead of the straight ORM
+ cleaned_tagnames = list() #those that were actually updated
+ if action == 'remove':
+ logging.debug('deleting tag marks: %s' % ','.join(tagnames))
+ marked_ts.delete()
+ else:
+ marked_names = marked_ts.values_list('tag__name', flat = True)
+ if len(marked_names) < len(tagnames):
+ unmarked_names = set(tagnames).difference(set(marked_names))
+ ts = Tag.objects.filter(name__in = unmarked_names)
+ new_marks = list()
+ for tag in ts:
+ MarkedTag(
+ user = self,
+ reason = reason,
+ tag = tag
+ ).save()
+ new_marks.append(tag.name)
+ cleaned_tagnames.extend(marked_names)
+ cleaned_tagnames.extend(new_marks)
+ else:
+ marked_ts.update(reason=reason)
+ cleaned_tagnames = tagnames
+
+ return cleaned_tagnames, cleaned_wildcards
+
@auto_now_timestamp
def user_retag_question(
self,
@@ -1651,6 +1729,42 @@ def user_receive_reputation(self, num_points):
else:
self.reputation = const.MIN_REPUTATION
+def user_update_wildcard_tag_selections(
+ self,
+ action = None,
+ reason = None,
+ wildcards = None,
+ ):
+ """updates the user selection of wildcard tags
+ and saves the user object to the database
+ """
+ new_tags = set(wildcards)
+ interesting = set(self.interesting_tags.split())
+ ignored = set(self.ignored_tags.split())
+
+ target_set = interesting
+ other_set = ignored
+ if reason == 'good':
+ pass
+ elif reason == 'bad':
+ target_set = ignored
+ other_set = interesting
+ else:
+ assert(action == 'remove')
+
+ if action == 'add':
+ target_set.update(new_tags)
+ other_set.difference_update(new_tags)
+ else:
+ target_set.difference_update(new_tags)
+ other_set.difference_update(new_tags)
+
+ self.interesting_tags = ' '.join(interesting)
+ self.ignored_tags = ' '.join(ignored)
+ self.save()
+ return new_tags
+
+
User.add_to_class('is_username_taken',classmethod(user_is_username_taken))
User.add_to_class(
'get_q_sel_email_feed_frequency',
@@ -1684,6 +1798,7 @@ User.add_to_class('delete_messages', delete_messages)
User.add_to_class('toggle_favorite_question', toggle_favorite_question)
User.add_to_class('follow_question', user_follow_question)
User.add_to_class('unfollow_question', user_unfollow_question)
+User.add_to_class('mark_tags', user_mark_tags)
User.add_to_class('is_following', user_is_following)
User.add_to_class('decrement_response_count', user_decrement_response_count)
User.add_to_class('increment_response_count', user_increment_response_count)
@@ -1699,6 +1814,7 @@ User.add_to_class('is_suspended', user_is_suspended)
User.add_to_class('is_blocked', user_is_blocked)
User.add_to_class('is_owner_of', user_is_owner_of)
User.add_to_class('can_moderate_user', user_can_moderate_user)
+User.add_to_class('has_affinity_to_question', user_has_affinity_to_question)
User.add_to_class('moderate_user_reputation', user_moderate_user_reputation)
User.add_to_class('set_status', user_set_status)
User.add_to_class('get_status_display', user_get_status_display)
@@ -1712,6 +1828,10 @@ User.add_to_class('close_question', user_close_question)
User.add_to_class('reopen_question', user_reopen_question)
User.add_to_class('accept_best_answer', user_accept_best_answer)
User.add_to_class('unaccept_best_answer', user_unaccept_best_answer)
+User.add_to_class(
+ 'update_wildcard_tag_selections',
+ user_update_wildcard_tag_selections
+)
#assertions
User.add_to_class('assert_can_vote_for_post', user_assert_can_vote_for_post)
@@ -2168,8 +2288,23 @@ def record_user_full_updated(instance, **kwargs):
)
activity.save()
+def complete_pending_tag_subscriptions(sender, request, *args, **kwargs):
+ """save pending tag subscriptions saved in the session"""
+ if 'subscribe_for_tags' in request.session:
+ (pure_tag_names, wildcards) = request.session.pop('subscribe_for_tags')
+ request.user.mark_tags(
+ pure_tag_names,
+ wildcards,
+ reason = 'good',
+ action = 'add'
+ )
+ request.user.message_set.create(
+ message = _('Your tag subscription was saved, thanks!')
+ )
+
def post_stored_anonymous_content(
sender,
+ request,
user,
session_key,
signal,
@@ -2237,6 +2372,7 @@ signals.flag_offensive.connect(record_flag_offensive, sender=Answer)
signals.tags_updated.connect(record_update_tags)
signals.user_updated.connect(record_user_full_updated, sender=User)
signals.user_logged_in.connect(post_stored_anonymous_content)
+signals.user_logged_in.connect(complete_pending_tag_subscriptions)
signals.post_updated.connect(
record_post_update_activity,
sender=Comment
diff --git a/askbot/models/content.py b/askbot/models/content.py
index 30164b1b..2b5045b0 100644
--- a/askbot/models/content.py
+++ b/askbot/models/content.py
@@ -4,6 +4,7 @@ from django.contrib.contenttypes import generic
from django.db import models
from askbot.models.meta import Comment, Vote
from askbot.models.user import EmailFeedSetting
+from askbot.models.tag import Tag
from django.utils import html as html_utils
class Content(models.Model):
@@ -184,23 +185,6 @@ class Content(models.Model):
#print 'final subscriber set is ', subscriber_set
return list(subscriber_set)
- def passes_tag_filter_for_user(user):
-
- post_tags = self.get_origin_post().tags.all()
-
- if user.tag_filter_setting == 'ignored':
- ignored_tags = user.tag_selections.filter(reason = 'bad')
- if set(post_tags) & set(ignored_tags):
- return False
- else:
- return True
- else:
- interesting_tags = user.tag_selections.filter(reason = 'good')
- if set(post_tags) & set(interesting_tags):
- return True
- else:
- return False
-
def get_latest_revision(self):
return self.revisions.all().order_by('-revised_at')[0]
@@ -236,30 +220,19 @@ class Content(models.Model):
return list(authors)
def passes_tag_filter_for_user(self, user):
- tags = self.get_origin_post().tags.all()
+ question = self.get_origin_post()
if user.tag_filter_setting == 'interesting':
#at least some of the tags must be marked interesting
- interesting_selections = user.tag_selections.filter(
- tag__in = tags,
- reason = 'good'
- )
- if interesting_selections.count() > 0:
- return True
- else:
- return False
-
+ return user.has_affinity_to_question(
+ question,
+ affinity_type = 'like'
+ )
elif user.tag_filter_setting == 'ignored':
- #at least one tag must be ignored
- ignored_selections = user.tag_selections.filter(
- tag__in = tags,
- reason = 'bad'
- )
- if ignored_selections.count() > 0:
- return False
- else:
- return True
-
+ return not user.has_affinity_to_question(
+ question,
+ affinity_type = 'dislike'
+ )
else:
raise ValueError(
'unexpected User.tag_filter_setting %s' \
diff --git a/askbot/models/question.py b/askbot/models/question.py
index f1612e88..6931dd3b 100644
--- a/askbot/models/question.py
+++ b/askbot/models/question.py
@@ -257,6 +257,12 @@ class QuestionManager(models.Manager):
search_state = search_state,
ignored_tag_names = ignored_tag_names
)
+ if askbot_settings.USE_WILDCARD_TAGS == True \
+ and request_user.is_authenticated() == True:
+ tagnames = request_user.interesting_tags
+ meta_data['interesting_tag_names'].extend(tagnames.split())
+ tagnames = request_user.ignored_tags
+ meta_data['ignored_tag_names'].extend(tagnames.split())
return qs, meta_data, related_tags
#todo: this function is similar to get_response_receivers
@@ -433,6 +439,15 @@ class Question(content.Content, DeletableContent):
"""
Updates Tag associations for a question to match the given
tagname string.
+
+ When tags are removed and their use count hits 0 - the tag is
+ automatically deleted.
+
+ When an added tag does not exist - it is created
+
+ Tag use counts are recalculated
+
+ A signal tags updated is sent
"""
previous_tags = list(self.tags.all())
diff --git a/askbot/models/tag.py b/askbot/models/tag.py
index d16436ea..e94b8452 100644
--- a/askbot/models/tag.py
+++ b/askbot/models/tag.py
@@ -29,11 +29,26 @@ class TagManager(models.Manager):
cursor.execute(query, [tag.id for tag in tags])
transaction.commit_unless_managed()
+
+ 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:
+ 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,
- questions=None,
- search_state=None,
- ignored_tag_names=None
+ questions = None,
+ search_state = None,
+ ignored_tag_names = None
):
"""must return at least tag names, along with use counts
handle several cases to optimize the query performance
diff --git a/askbot/skins/default/media/js/live_search.js b/askbot/skins/default/media/js/live_search.js
index 19c5f8a4..6c31bea6 100644
--- a/askbot/skins/default/media/js/live_search.js
+++ b/askbot/skins/default/media/js/live_search.js
@@ -184,33 +184,11 @@ $(document).ready(function(){
};
var render_tag = function(tag_name, linkable, deletable){
- var url = askbot['urls']['questions'] +
- '?tags=' + encodeURI(tag_name);
- var tag_title = $.i18n._(
- "see questions tagged '{tag}'"
- ).replace(
- '{tag}',
- tag_name
- );
- var tag_element = 'span';
- var tag_url = '';
- if (linkable){
- tag_element = 'a';
- tag_url = ' href="' + url + '" ';
- }
- html = '<' + tag_element +
- ' class="tag tag-right" ' +
- tag_url +
- ' title="' + tag_title + '" rel="tag"' +
- '>' + tag_name + '</' + tag_element + '>';
- if (deletable){
- html += '<span class="delete-icon"></span>';
- }
- var tag_class = 'tag-left';
- if (deletable){
- tag_class += ' deletable-tag';
- }
- return '<li class="' + tag_class + '">' + html + '</li>';
+ var tag = new Tag();
+ tag.setName(tag_name);
+ tag.setDeletable(deletable);
+ tag.setLinkable(linkable);
+ return tag.getElement().outerHTML();
};
var render_tags = function(tags, linkable, deletable){
@@ -316,10 +294,18 @@ $(document).ready(function(){
var search_tags = $('#search-tags');
search_tags.children().remove();
var tags_html = '';
- $.each(tags, function(idx, tag){
- tags_html += render_tag(tag, false, true);
+ $.each(tags, function(idx, tag_name){
+ var tag = new Tag();
+ tag.setName(tag_name);
+ tag.setDeletable(true);
+ tag.setLinkable(false);
+ tag.setDeleteHandler(
+ function(){
+ remove_search_tag(tag_name);
+ }
+ );
+ search_tags.append(tag.getElement());
});
- search_tags.html(tags_html);
};
var create_relevance_tab = function(){
@@ -387,13 +373,17 @@ $(document).ready(function(){
});
};
- var activate_search_tag_deleters = function(){
- var deleters = $('#search-tags .delete-icon');
- $.each(deleters, function(idx, deleter){
- var search_tag = $(deleter).prev().html();
- setupButtonEventHandlers(
- $(deleter),
- function(){remove_search_tag(search_tag)}
+ var activate_search_tags = function(){
+ var search_tags = $('#search-tags .tag-left');
+ $.each(search_tags, function(idx, element){
+ var tag = new Tag();
+ tag.decorate($(element));
+ //todo: setDeleteHandler and setHandler
+ //must work after decorate & must have getName
+ tag.setDeleteHandler(
+ function(){
+ remove_search_tag(tag.getName());
+ }
);
});
};
@@ -411,7 +401,6 @@ $(document).ready(function(){
render_paginator(data['paginator']);
set_question_count(data['question_counter']);
render_search_tags(data['query_data']['tags']);
- activate_search_tag_deleters();
render_faces(data['faces']);
render_related_tags(data['related_tags']);
render_relevance_sort_tab();
@@ -450,6 +439,6 @@ $(document).ready(function(){
prev_text = '';
}
- activate_search_tag_deleters();
+ activate_search_tags();
listen();
});
diff --git a/askbot/skins/default/media/js/post.js b/askbot/skins/default/media/js/post.js
index 864ac77b..17fdb0ee 100644
--- a/askbot/skins/default/media/js/post.js
+++ b/askbot/skins/default/media/js/post.js
@@ -626,21 +626,9 @@ var questionRetagger = function(){
var render_tag = function(tag_name){
//copy-paste from live search!!!
- var url = askbot['urls']['questions'] +
- '?tags=' + encodeURI(tag_name);
- var tag_title = $.i18n._(
- "see questions tagged '{tag}'"
- ).replace(
- '{tag}',
- tag_name
- );
- return '<li class="tag-left">' +
- '<a ' +
- 'class="tag tag-right" ' +
- 'href="' + url + '" ' +
- 'title="' + tag_title + '" rel="tag"' +
- '>' + tag_name + '</a>' +
- '</li>';
+ var tag = new Tag();
+ tag.setName(tag_name);
+ return tag.getElement().outerHTML();
};
var drawNewTags = function(new_tags){
@@ -801,81 +789,6 @@ var questionRetagger = function(){
};
}();
-inherits = function(childCtor, parentCtor) {
- /** @constructor taken from google closure */
- function tempCtor() {};
- tempCtor.prototype = parentCtor.prototype;
- childCtor.superClass_ = parentCtor.prototype;
- childCtor.prototype = new tempCtor();
- childCtor.prototype.constructor = childCtor;
-};
-
-/* wrapper around jQuery object */
-var WrappedElement = function(){
- this._element = null;
-};
-WrappedElement.prototype.setElement = function(element){
- this._element = element;
-};
-WrappedElement.prototype.createDom = function(){
- this._element = $('<div></div>');
-};
-WrappedElement.prototype.getElement = function(){
- if (this._element === null){
- this.createDom();
- }
- return this._element;
-};
-WrappedElement.prototype.dispose = function(){
- this._element.remove();
-};
-
-var SimpleControl = function(){
- WrappedElement.call(this);
- this._handler = null;
-};
-inherits(SimpleControl, WrappedElement);
-
-SimpleControl.prototype.setHandler = function(handler){
- this._handler = handler;
-};
-
-var EditLink = function(){
- SimpleControl.call(this)
-};
-inherits(EditLink, SimpleControl);
-
-EditLink.prototype.createDom = function(){
- var element = $('<a></a>');
- element.addClass('edit');
- this.decorate(element);
-};
-
-EditLink.prototype.decorate = function(element){
- this._element = element;
- this._element.attr('title', $.i18n._('click to edit this comment'));
- this._element.html($.i18n._('edit'));
- setupButtonEventHandlers(this._element, this._handler);
-};
-
-var DeleteIcon = function(title){
- SimpleControl.call(this);
- this._title = title;
-};
-inherits(DeleteIcon, SimpleControl);
-
-DeleteIcon.prototype.decorate = function(element){
- this._element = element;
- this._element.attr('class', 'delete-icon');
- this._element.attr('title', this._title);
- setupButtonEventHandlers(this._element, this._handler);
-};
-
-DeleteIcon.prototype.createDom = function(){
- this.decorate($('<span />'));
-};
-
-
//constructor for the form
var EditCommentForm = function(){
WrappedElement.call(this);
diff --git a/askbot/skins/default/media/js/tag_selector.js b/askbot/skins/default/media/js/tag_selector.js
index df444627..9ee8a6e7 100644
--- a/askbot/skins/default/media/js/tag_selector.js
+++ b/askbot/skins/default/media/js/tag_selector.js
@@ -1,5 +1,87 @@
//var interestingTags, ignoredTags, tags, $;
+var TagDetailBox = function(box_type){
+ WrappedElement.call(this);
+ this.box_type = box_type;
+ this._is_blank = true;
+ this._tags = new Array();
+ this.wildcard = undefined;
+};
+inherits(TagDetailBox, WrappedElement);
+
+TagDetailBox.prototype.createDom = function(){
+ this._element = this.makeElement('div');
+ this._element.addClass('wildcard-tags');
+ this._headline = this.makeElement('p');
+ this._element.append(this._headline);
+ this._headline.html($.i18n._('Tag "<span></span>" matches:'));
+ this._tag_list_element = this.makeElement('ul');
+ this._tag_list_element.addClass('tags');
+ this._element.append(this._tag_list_element);
+}
+
+TagDetailBox.prototype.belongsTo = function(wildcard){
+ return (this.wildcard === wildcard);
+};
+
+TagDetailBox.prototype.isBlank = function(){
+ return this._is_blank;
+};
+
+TagDetailBox.prototype.clear = function(){
+ if (this.isBlank()){
+ return;
+ }
+ this._is_blank = true;
+ this.getElement().hide();
+ this.wildcard = null;
+ $.each(this._tags, function(idx, item){
+ item.dispose();
+ });
+ this._tags = new Array();
+};
+
+TagDetailBox.prototype.loadTags = function(wildcard, callback){
+ $.ajax({
+ type: 'GET',
+ dataType: 'json',
+ cache: false,
+ url: askbot['urls']['get_tags_by_wildcard'],
+ data: { wildcard: wildcard },
+ success: callback
+ });
+};
+
+TagDetailBox.prototype.renderFor = function(wildcard){
+ var me = this;
+ this.loadTags(
+ wildcard,
+ function(data, text_status, xhr){
+ me._tag_names = data['tag_names'];
+ if (data['tag_count'] > 0){
+ me._element.show();
+ me._headline.find('span').html(wildcard.replace(/\*$/, '&#10045;'));
+ $.each(me._tag_names, function(idx, name){
+ var tag = new Tag();
+ tag.setName(name);
+ tag.setLinkable(false);
+ me._tags.push(tag);
+ me._tag_list_element.append(tag.getElement());
+ });
+ me._is_blank = false;
+ me.wildcard = wildcard;
+ } else {
+ me.clear();
+ }
+ }
+ );
+}
+
function pickedTags(){
+
+ var interestingTags = {};
+ var ignoredTags = {};
+ var interestingTagDetailBox = new TagDetailBox('interesting');
+ var ignoredTagDetailBox = new TagDetailBox('ignored');
var sendAjax = function(tagnames, reason, action, callback){
var url = '';
@@ -41,39 +123,77 @@ function pickedTags(){
}
};
- var setupTagDeleteEvents = function(obj,tag_store,tagname,reason,send_ajax){
- obj.click( function(){
- unpickTag(tag_store,tagname,reason,send_ajax);
- });
+ var getTagList = function(reason){
+ var base_selector = '.marked-tags';
+ if (reason === 'good'){
+ var extra_selector = '.interesting';
+ } else {
+ var extra_selector = '.ignored';
+ }
+ return $(base_selector + extra_selector);
+ };
+
+ var getWildcardTagDetailBox = function(reason){
+ if (reason === 'good'){
+ return interestingTagDetailBox;
+ } else {
+ return ignoredTagDetailBox;
+ }
+ };
+
+ var handleWildcardTagClick = function(tag_name, reason){
+ var detail_box = getWildcardTagDetailBox(reason);
+ var tag_box = getTagList(reason);
+ if (detail_box.isBlank()){
+ detail_box.renderFor(tag_name);
+ } else if (detail_box.belongsTo(tag_name)){
+ detail_box.clear();//toggle off
+ } else {
+ detail_box.clear();//redraw with new data
+ detail_box.renderFor(tag_name);
+ }
+ if (!detail_box.inDocument()){
+ tag_box.after(detail_box.getElement());
+ detail_box.enterDocument();
+ }
};
var renderNewTags = function(
- clean_tagnames,
- reason,
- to_target,
- to_tag_container
- ){
- $.each(clean_tagnames, function(idx, tagname){
- var new_tag = $('<li></li>');
- new_tag.addClass('deletable-tag');
- new_tag.addClass('tag-left');
- var tag_link = $('<a></a>');
- tag_link.addClass('tag-right');
- tag_link.addClass('tag')
- tag_link.attr('rel','tag');
- var tag_url = askbot['urls']['questions'] + '?tags=' + tagname;
- tag_link.attr('href', tag_url);
- tag_link.html(tagname);
- var del_link = $('<span></span>');
- del_link.addClass('delete-icon');
-
- setupTagDeleteEvents(del_link, to_target, tagname, reason, true);
-
- new_tag.append(tag_link);
- new_tag.append(del_link);
- to_tag_container.append(new_tag);
-
- to_target[tagname] = new_tag;
+ clean_tag_names,
+ reason,
+ to_target,
+ to_tag_container
+ ){
+ $.each(clean_tag_names, function(idx, tag_name){
+ var tag = new Tag();
+ tag.setName(tag_name);
+ tag.setDeletable(true);
+
+ if (/\*$/.test(tag_name)){
+ tag.setLinkable(false);
+ var detail_box = getWildcardTagDetailBox(reason);
+ tag.setHandler(function(){
+ handleWildcardTagClick(tag_name, reason);
+ if (detail_box.belongsTo(tag_name)){
+ detail_box.clear();
+ }
+ });
+ var delete_handler = function(){
+ unpickTag(to_target, tag_name, reason, true);
+ if (detail_box.belongsTo(tag_name)){
+ detail_box.clear();
+ }
+ }
+ } else {
+ var delete_handler = function(){
+ unpickTag(to_target, tag_name, reason, true);
+ }
+ }
+
+ tag.setDeleteHandler(delete_handler);
+ var tag_element = tag.getElement();
+ to_tag_container.append(tag_element);
+ to_target[tag_name] = tag_element;
});
};
@@ -127,8 +247,6 @@ function pickedTags(){
};
var collectPickedTags = function(section){
- interestingTags = {};
- ignoredTags = {};
if (section === 'interesting'){
var reason = 'good';
var tag_store = interestingTags;
@@ -140,17 +258,24 @@ function pickedTags(){
else {
return;
}
- $('.' + section + '.tags.marked-tags a.tag').each(
+ $('.' + section + '.tags.marked-tags .tag-left').each(
function(i,item){
- var tag_name = $(item).html();
- tag_store[tag_name] = $(item).parent();
- setupTagDeleteEvents(
- $(item).parent().find('.delete-icon'),
- tag_store,
- tag_name,
- reason,
- true
- );
+ var tag = new Tag();
+ tag.decorate($(item));
+ tag.setDeleteHandler(function(){
+ unpickTag(
+ tag_store,
+ tag.getName(),
+ reason,
+ true
+ )
+ });
+ if (tag.isWildcard()){
+ tag.setHandler(function(){
+ handleWildcardTagClick(tag.getName(), reason)
+ });
+ }
+ tag_store[tag.getName()] = $(item);
}
);
};
diff --git a/askbot/skins/default/media/js/utils.js b/askbot/skins/default/media/js/utils.js
index 40da4271..2d59abd2 100644
--- a/askbot/skins/default/media/js/utils.js
+++ b/askbot/skins/default/media/js/utils.js
@@ -31,6 +31,17 @@ var showMessage = function(element, msg, where) {
div.fadeIn("fast");
};
+//outer html hack - https://github.com/brandonaaron/jquery-outerhtml/
+(function($){
+ var div;
+ $.fn.outerHTML = function() {
+ var elem = this[0],
+ tmp;
+ return !elem ? null
+ : typeof ( tmp = elem.outerHTML ) === 'string' ? tmp
+ : ( div = div || $('<div/>') ).html( this.eq(0).clone() ).html();
+ };
+})(jQuery);
var makeKeyHandler = function(key, callback){
return function(e){
@@ -90,6 +101,269 @@ var notify = function() {
};
} ();
+/* some google closure-like code for the ui elements */
+var inherits = function(childCtor, parentCtor) {
+ /** @constructor taken from google closure */
+ function tempCtor() {};
+ tempCtor.prototype = parentCtor.prototype;
+ childCtor.superClass_ = parentCtor.prototype;
+ childCtor.prototype = new tempCtor();
+ childCtor.prototype.constructor = childCtor;
+};
+
+/* wrapper around jQuery object */
+var WrappedElement = function(){
+ this._element = null;
+ this._in_document = false;
+};
+WrappedElement.prototype.setElement = function(element){
+ this._element = element;
+};
+WrappedElement.prototype.createDom = function(){
+ this._element = $('<div></div>');
+};
+WrappedElement.prototype.getElement = function(){
+ if (this._element === null){
+ this.createDom();
+ }
+ return this._element;
+};
+WrappedElement.prototype.inDocument = function(){
+ return this._in_document;
+};
+WrappedElement.prototype.enterDocument = function(){
+ return this._in_document = true;
+};
+WrappedElement.prototype.hasElement = function(){
+ return (this._element !== null);
+};
+WrappedElement.prototype.makeElement = function(html_tag){
+ //makes jQuery element with tags
+ return $('<' + html_tag + '></' + html_tag + '>');
+};
+WrappedElement.prototype.dispose = function(){
+ this._element.remove();
+ this._in_document = false;
+};
+
+var SimpleControl = function(){
+ WrappedElement.call(this);
+ this._handler = null;
+ this._title = null;
+};
+inherits(SimpleControl, WrappedElement);
+
+SimpleControl.prototype.setHandler = function(handler){
+ this._handler = handler;
+ if (this.hasElement()){
+ this.setHandlerInternal();
+ }
+};
+
+SimpleControl.prototype.setHandlerInternal = function(){
+ //default internal setHandler behavior
+ setupButtonEventHandlers(this._element, this._handler);
+};
+
+SimpleControl.prototype.setTitle = function(title){
+ this._title = title;
+};
+
+var EditLink = function(){
+ SimpleControl.call(this)
+};
+inherits(EditLink, SimpleControl);
+
+EditLink.prototype.createDom = function(){
+ var element = $('<a></a>');
+ element.addClass('edit');
+ this.decorate(element);
+};
+
+EditLink.prototype.decorate = function(element){
+ this._element = element;
+ this._element.attr('title', $.i18n._('click to edit this comment'));
+ this._element.html($.i18n._('edit'));
+ this.setHandlerInternal();
+};
+
+var DeleteIcon = function(title){
+ SimpleControl.call(this);
+ this._title = title;
+};
+inherits(DeleteIcon, SimpleControl);
+
+DeleteIcon.prototype.decorate = function(element){
+ this._element = element;
+ this._element.attr('class', 'delete-icon');
+ this._element.attr('title', this._title);
+ if (this._handler !== null){
+ this.setHandlerInternal();
+ }
+};
+
+DeleteIcon.prototype.setHandlerInternal = function(){
+ setupButtonEventHandlers(this._element, this._handler);
+};
+
+DeleteIcon.prototype.createDom = function(){
+ this._element = this.makeElement('span');
+ this.decorate(this._element);
+};
+
+var Tag = function(){
+ SimpleControl.call(this);
+ this._deletable = false;
+ this._delete_handler = null;
+ this._delete_icon_title = null;
+ this._tag_title = null;
+ this._name = null;
+ this._url_params = null;
+ this._inner_html_tag = 'a';
+ this._html_tag = 'li';
+}
+inherits(Tag, SimpleControl);
+
+Tag.prototype.setName = function(name){
+ this._name = name;
+};
+
+Tag.prototype.getName = function(){
+ return this._name;
+};
+
+Tag.prototype.setHtmlTag = function(html_tag){
+ this._html_tag = html_tag;
+};
+
+Tag.prototype.setDeletable = function(is_deletable){
+ this._deletable = is_deletable;
+};
+
+Tag.prototype.setLinkable = function(is_linkable){
+ if (is_linkable === true){
+ this._inner_html_tag = 'a';
+ } else {
+ this._inner_html_tag = 'span';
+ }
+};
+
+Tag.prototype.isLinkable = function(){
+ return (this._inner_html_tag === 'a');
+};
+
+Tag.prototype.isDeletable = function(){
+ return this._deletable;
+};
+
+Tag.prototype.isWildcard = function(){
+ return (this.getName().substr(-1) === '*');
+};
+
+Tag.prototype.setUrlParams = function(url_params){
+ this._url_params = url_params;
+};
+
+Tag.prototype.setHandlerInternal = function(){
+ setupButtonEventHandlers(this._element.find('.tag'), this._handler);
+};
+
+/* delete handler will be specific to the task */
+Tag.prototype.setDeleteHandler = function(delete_handler){
+ this._delete_handler = delete_handler;
+ if (this.hasElement() && this.isDeletable()){
+ this._delete_icon.setHandler(delete_handler);
+ }
+};
+
+Tag.prototype.getDeleteHandler = function(){
+ return this._delete_handler;
+};
+
+Tag.prototype.setDeleteIconTitle = function(title){
+ this._delete_icon_title = title;
+};
+
+Tag.prototype.decorate = function(element){
+ this._element = element;
+ var del = element.find('.delete-icon');
+ if (del.length === 1){
+ this.setDeletable(true);
+ this._delete_icon = new DeleteIcon();
+ if (this._delete_icon_title != null){
+ this._delete_icon.setTitle(this._delete_icon_title);
+ }
+ //do not set the delete handler here
+ this._delete_icon.decorate(del);
+ }
+ this._inner_element = this._element.find('.tag');
+ this._name = this.decodeTagName($.trim(this._inner_element.html()));
+ if (this._title !== null){
+ this._inner_element.attr('title', this._title);
+ }
+ if (this._handler !== null){
+ this.setHandlerInternal();
+ }
+};
+
+Tag.prototype.getDisplayTagName = function(){
+ //replaces the trailing * symbol with the unicode asterisk
+ return this._name.replace(/\*$/, '&#10045;');
+};
+
+Tag.prototype.decodeTagName = function(encoded_name){
+ return encoded_name.replace('\u273d', '*');
+};
+
+Tag.prototype.createDom = function(){
+ this._element = this.makeElement(this._html_tag);
+ //render the outer element
+ if (this._deletable){
+ this._element.addClass('deletable-tag');
+ }
+ this._element.addClass('tag-left');
+
+ //render the inner element
+ this._inner_element = this.makeElement(this._inner_html_tag);
+ if (this.isLinkable()){
+ var url = askbot['urls']['questions'];
+ url += '?tags=' + escape(this.getName());
+ if (this._url_params !== null){
+ url += escape('&' + this._url_params);
+ }
+ this._inner_element.attr('href', url);
+ }
+ this._inner_element.addClass('tag tag-right');
+ this._inner_element.attr('rel', 'tag');
+ if (this._title === null){
+ this.setTitle(
+ $.i18n._(
+ "see questions tagged '{tag}'"
+ ).replace(
+ '{tag}',
+ this.getName()
+ )
+ );
+ }
+ this._inner_element.attr('title', this._title);
+ this._inner_element.html(this.getDisplayTagName());
+
+ this._element.append(this._inner_element);
+
+ if (!this.isLinkable() && this._handler !== null){
+ this.setHandlerInternal();
+ }
+
+ if (this._deletable){
+ this._delete_icon = new DeleteIcon();
+ this._delete_icon.setHandler(this.getDeleteHandler());
+ if (this._delete_icon_title !== null){
+ this._delete_icon.setTitle(this._delete_icon_title);
+ }
+ this._element.append(this._delete_icon.getElement());
+ }
+};
+
//Search Engine Keyword Highlight with Javascript
//http://scott.yang.id.au/code/se-hilite/
Hilite={elementid:"content",exact:true,max_nodes:1000,onload:true,style_name:"hilite",style_name_suffix:true,debug_referrer:""};Hilite.search_engines=[["local","q"],["cnprog\\.","q"],["google\\.","q"],["search\\.yahoo\\.","p"],["search\\.msn\\.","q"],["search\\.live\\.","query"],["search\\.aol\\.","userQuery"],["ask\\.com","q"],["altavista\\.","q"],["feedster\\.","q"],["search\\.lycos\\.","q"],["alltheweb\\.","q"],["technorati\\.com/search/([^\\?/]+)",1],["dogpile\\.com/info\\.dogpl/search/web/([^\\?/]+)",1,true]];Hilite.decodeReferrer=function(d){var g=null;var e=new RegExp("");for(var c=0;c<Hilite.search_engines.length;c++){var f=Hilite.search_engines[c];e.compile("^http://(www\\.)?"+f[0],"i");var b=d.match(e);if(b){var a;if(isNaN(f[1])){a=Hilite.decodeReferrerQS(d,f[1])}else{a=b[f[1]+1]}if(a){a=decodeURIComponent(a);if(f.length>2&&f[2]){a=decodeURIComponent(a)}a=a.replace(/\'|"/g,"");a=a.split(/[\s,\+\.]+/);return a}break}}return null};Hilite.decodeReferrerQS=function(f,d){var b=f.indexOf("?");var c;if(b>=0){var a=new String(f.substring(b+1));b=0;c=0;while((b>=0)&&((c=a.indexOf("=",b))>=0)){var e,g;e=a.substring(b,c);b=a.indexOf("&",c)+1;if(e==d){if(b<=0){return a.substring(c+1)}else{return a.substring(c+1,b-1)}}else{if(b<=0){return null}}}}return null};Hilite.hiliteElement=function(f,e){if(!e||f.childNodes.length==0){return}var c=new Array();for(var b=0;b<e.length;b++){e[b]=e[b].toLowerCase();if(Hilite.exact){c.push("\\b"+e[b]+"\\b")}else{c.push(e[b])}}c=new RegExp(c.join("|"),"i");var a={};for(var b=0;b<e.length;b++){if(Hilite.style_name_suffix){a[e[b]]=Hilite.style_name+(b+1)}else{a[e[b]]=Hilite.style_name}}var d=function(m){var j=c.exec(m.data);if(j){var n=j[0];var i="";var h=m.splitText(j.index);var g=h.splitText(n.length);var l=m.ownerDocument.createElement("SPAN");m.parentNode.replaceChild(l,h);l.className=a[n.toLowerCase()];l.appendChild(h);return l}else{return m}};Hilite.walkElements(f.childNodes[0],1,d)};Hilite.hilite=function(){var a=Hilite.debug_referrer?Hilite.debug_referrer:document.referrer;var b=null;a=Hilite.decodeReferrer(a);if(a&&((Hilite.elementid&&(b=document.getElementById(Hilite.elementid)))||(b=document.body))){Hilite.hiliteElement(b,a)}};Hilite.walkElements=function(d,f,e){var a=/^(script|style|textarea)/i;var c=0;while(d&&f>0){c++;if(c>=Hilite.max_nodes){var b=function(){Hilite.walkElements(d,f,e)};setTimeout(b,50);return}if(d.nodeType==1){if(!a.test(d.tagName)&&d.childNodes.length>0){d=d.childNodes[0];f++;continue}}else{if(d.nodeType==3){d=e(d)}}if(d.nextSibling){d=d.nextSibling}else{while(f>0){d=d.parentNode;f--;if(d.nextSibling){d=d.nextSibling;break}}}}};if(Hilite.onload){if(window.attachEvent){window.attachEvent("onload",Hilite.hilite)}else{if(window.addEventListener){window.addEventListener("load",Hilite.hilite,false)}else{var __onload=window.onload;window.onload=function(){Hilite.hilite();__onload()}}}};
diff --git a/askbot/skins/default/media/style/style.css b/askbot/skins/default/media/style/style.css
index 77e5b0c2..6f745357 100755
--- a/askbot/skins/default/media/style/style.css
+++ b/askbot/skins/default/media/style/style.css
@@ -36,9 +36,9 @@ input, select {
}
p {
- margin-bottom: 13px;
font-size: 14px;
line-height: 140%;
+ margin-bottom: 6px;
padding-left: 5px;
}
@@ -341,6 +341,7 @@ blockquote {
askbot/models/__init__.py:format_instant_notification_email()
*/
ul.tags,
+.boxC ul.tags,
ul.tags.marked-tags,
ul#related-tags {
list-style: none;
@@ -357,7 +358,12 @@ ul.tags li {
padding: 0;
}
-ul.tags.marked-tags li {
+.wildcard-tags {
+ clear: both;
+}
+
+ul.tags.marked-tags li,
+.wildcard-tags ul.tags li {
margin-bottom: 5px;
}
@@ -381,6 +387,7 @@ ul#related-tags li {
.tag-left {
background: url(../images/tag-right.png) no-repeat right center;
border: none;
+ cursor: pointer;
display: block;
float: left;
height: 18px;
diff --git a/askbot/skins/default/templates/blocks/bottom_scripts.html b/askbot/skins/default/templates/blocks/bottom_scripts.html
index 3bbedc3d..4d3b8afe 100644
--- a/askbot/skins/default/templates/blocks/bottom_scripts.html
+++ b/askbot/skins/default/templates/blocks/bottom_scripts.html
@@ -18,6 +18,7 @@
askbot['data']['userIsAuthenticated'] = false;
{% endif %}
askbot['urls']['mark_read_message'] = '{% url "read_message" %}';
+ askbot['urls']['get_tags_by_wildcard'] = '{% url "get_tags_by_wildcard" %}';
</script>
<script
type="text/javascript"
diff --git a/askbot/skins/default/templates/macros.html b/askbot/skins/default/templates/macros.html
index 028937f8..d82feb2f 100644
--- a/askbot/skins/default/templates/macros.html
+++ b/askbot/skins/default/templates/macros.html
@@ -283,21 +283,21 @@ poor design of the data or methods on data objects #}
</ul>
{%- endmacro -%}
-{# todo: remove css_class argument because it is redundant #}
+{# todo: remove the extra content argument to make its usage more explicit #}
{%- macro tag_widget(
tag,
deletable = False,
is_link = True,
delete_link_title = None,
+ css_class = None,
url_params = None,
html_tag = 'div',
- css_class = None,
extra_content = ''
)
-%}
{% spaceless %}
<{{ html_tag }} class="tag-left{% if deletable %} deletable-tag{% endif %}">
- <{{ if_else(is_link, 'a', 'span') }}
+ <{% if not is_link or tag[-1] == '*' %}span{% else %}a{% endif %}
class="tag tag-right{% if css_class %} {{ css_class }}{% endif %}"
href="{% url questions %}?tags={{tag|urlencode}}{{
if_else(
@@ -306,7 +306,8 @@ poor design of the data or methods on data objects #}
''
)|escape
}}"
- title="{% trans %}see questions tagged '{{ tag }}'{% endtrans %}" rel="tag">{{ tag }}</{{ if_else(is_link, 'a', 'span') }}>
+ title="{% trans %}see questions tagged '{{ tag }}'{% endtrans %}" rel="tag"
+ >{{ tag|replace('*', '&#10045;') }}</{% if not is_link or tag[-1] == '*' %}span{% else %}a{% endif %}>
{% if deletable %}
<span class="delete-icon"
{% if delete_link_title %}
diff --git a/askbot/skins/default/templates/subscribe_for_tags.html b/askbot/skins/default/templates/subscribe_for_tags.html
new file mode 100644
index 00000000..9a58ccbf
--- /dev/null
+++ b/askbot/skins/default/templates/subscribe_for_tags.html
@@ -0,0 +1,19 @@
+{% extends "two_column_body.html" %}
+{% import "macros.html" as macros %}
+{% block title %}{% trans %}Subscribe for tags{% endtrans %}{% endblock %}
+{% block content %}
+<h1>{% trans %}Subscribe for tags{% endtrans %}</h1>
+<p>{% trans %}Please, subscribe for the following tags:{% endtrans %}</p>
+<ul class="tags" style="margin-left: 4px">
+ {% for tag in tags %}
+ {{ macros.tag_widget(tag, html_tag = 'li', is_link = False) }}
+ {% endfor %}
+</ul>
+<div style="clear:both;padding-top: 5px">
+ <form method="post" action="{% url subscribe_for_tags %}">
+ <input type="hidden" name="tags" value="{{tags|join(' ')|escape}}" />
+ <input type="submit" name="ok" value="{% trans %}Subscribe{% endtrans %}" />
+ <input type="submit" name="nope" value="{% trans %}Cancel{% endtrans %}" />
+ </form>
+</div>
+{% endblock %}
diff --git a/askbot/tests/db_api_tests.py b/askbot/tests/db_api_tests.py
index fe09b283..e91e5986 100644
--- a/askbot/tests/db_api_tests.py
+++ b/askbot/tests/db_api_tests.py
@@ -5,6 +5,7 @@ e.g. ``some_user.do_something(...)``
"""
from askbot.tests.utils import AskbotTestCase
from askbot import models
+from askbot.conf import settings as askbot_settings
import datetime
class DBApiTests(AskbotTestCase):
@@ -140,3 +141,74 @@ class DBApiTests(AskbotTestCase):
count = models.Tag.objects.filter(name='one-tag').count()
self.assertEquals(count, 0)
+
+class UserLikeTests(AskbotTestCase):
+ def setUp(self):
+ self.create_user()
+ self.question = self.post_question(tags = 'one two three')
+
+ def test_user_likes_question_via_tags(self):
+ truth_table = (
+ ('good', 'like', True),
+ ('good', 'dislike', False),
+ ('bad', 'like', False),
+ ('bad', 'dislike', True),
+ )
+ tag = models.Tag.objects.get(name = 'one')
+ for item in truth_table:
+ reason = item[0]
+ mt = models.MarkedTag(user = self.user, tag = tag, reason = reason)
+ mt.save()
+ self.assertEquals(
+ self.user.has_affinity_to_question(
+ question = self.question,
+ affinity_type = item[1]
+ ),
+ item[2]
+ )
+ mt.delete()
+
+ def setup_wildcard(self, wildcard = None, reason = None):
+ if reason == 'good':
+ self.user.interesting_tags = wildcard
+ self.user.ignored_tags = ''
+ else:
+ self.user.ignored_tags = wildcard
+ self.user.interesting_tags = ''
+ self.user.save()
+ askbot_settings.update('USE_WILDCARD_TAGS', True)
+
+ def assert_affinity_is(self, affinity_type, expectation):
+ self.assertEquals(
+ self.user.has_affinity_to_question(
+ question = self.question,
+ affinity_type = affinity_type
+ ),
+ expectation
+ )
+
+ def test_user_likes_question_via_wildcards(self):
+ self.setup_wildcard('on*', 'good')
+ self.assert_affinity_is('like', True)
+ self.assert_affinity_is('dislike', False)
+
+ self.setup_wildcard('aouaou* o* on* oeu*', 'good')
+ self.assert_affinity_is('like', True)
+ self.assert_affinity_is('dislike', False)
+
+ self.setup_wildcard('on*', 'bad')
+ self.assert_affinity_is('like', False)
+ self.assert_affinity_is('dislike', True)
+
+ self.setup_wildcard('aouaou* o* on* oeu*', 'bad')
+ self.assert_affinity_is('like', False)
+ self.assert_affinity_is('dislike', True)
+
+ self.setup_wildcard('one*', 'good')
+ self.assert_affinity_is('like', True)
+ self.assert_affinity_is('dislike', False)
+
+ self.setup_wildcard('oneone*', 'good')
+ self.assert_affinity_is('like', False)
+ self.assert_affinity_is('dislike', False)
+
diff --git a/askbot/urls.py b/askbot/urls.py
index e721d81b..4ebec8c7 100644
--- a/askbot/urls.py
+++ b/askbot/urls.py
@@ -157,6 +157,16 @@ urlpatterns = patterns('',
name='unmark_tag'
),
url(
+ 'get-tags-by-wildcard/',
+ views.commands.get_tags_by_wildcard,
+ name = 'get_tags_by_wildcard'
+ ),
+ url(
+ r'^%s$' % _('subscribe-for-tags/'),
+ views.commands.subscribe_for_tags,
+ name = 'subscribe_for_tags'
+ ),
+ url(
r'^%s$' % _('users/'),
views.users.users,
name='users'
diff --git a/askbot/views/commands.py b/askbot/views/commands.py
index 28ffb34e..571f97d1 100644
--- a/askbot/views/commands.py
+++ b/askbot/views/commands.py
@@ -12,10 +12,10 @@ from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext as _
from askbot import models
-from askbot.forms import CloseForm
+from askbot import forms
from django.core.urlresolvers import reverse
from django.contrib.auth.decorators import login_required
-from askbot.utils.decorators import ajax_only, ajax_login_required
+from askbot.utils import decorators
from askbot.skins.loaders import render_into_skin
from askbot import const
import logging
@@ -327,38 +327,90 @@ def vote(request, id):
return HttpResponse(data, mimetype="application/json")
#internally grouped views - used by the tagging system
-@ajax_login_required
+@decorators.ajax_login_required
def mark_tag(request, **kwargs):#tagging system
action = kwargs['action']
post_data = simplejson.loads(request.raw_post_data)
- tagnames = post_data['tagnames']
- marked_ts = models.MarkedTag.objects.filter(
- user=request.user,
- tag__name__in=tagnames
- )
- #todo: use the user api methods here instead of the straight ORM
- if action == 'remove':
- logging.debug('deleting tag marks: %s' % ','.join(tagnames))
- marked_ts.delete()
- else:
- reason = kwargs['reason']
- if len(marked_ts) == 0:
- try:
- ts = models.Tag.objects.filter(name__in=tagnames)
- for tag in ts:
- mt = models.MarkedTag(
- user=request.user,
- reason=reason,
- tag=tag
- )
- mt.save()
- except:
- pass
+ raw_tagnames = post_data['tagnames']
+ reason = kwargs.get('reason', None)
+ #separate plain tag names and wildcard tags
+
+ tagnames, wildcards = forms.clean_marked_tagnames(raw_tagnames)
+ cleaned_tagnames, cleaned_wildcards = request.user.mark_tags(
+ tagnames,
+ wildcards,
+ reason = reason,
+ action = action
+ )
+
+ #lastly - calculate tag usage counts
+ tag_usage_counts = dict()
+ for name in tagnames:
+ if name in cleaned_tagnames:
+ tag_usage_counts[name] = 1
else:
- marked_ts.update(reason=reason)
- return HttpResponse(simplejson.dumps(''), mimetype="application/json")
+ tag_usage_counts[name] = 0
+
+ for name in wildcards:
+ if name in cleaned_wildcards:
+ tag_usage_counts[name] = models.Tag.objects.filter(
+ name__startswith = name[:-1]
+ ).count()
+ else:
+ tag_usage_counts[name] = 0
+
+ return HttpResponse(simplejson.dumps(tag_usage_counts), mimetype="application/json")
+
+#@decorators.ajax_only
+@decorators.get_only
+def get_tags_by_wildcard(request):
+ """returns an json encoded array of tag names
+ in the response to a wildcard tag name
+ """
+ matching_tags = models.Tag.objects.get_by_wildcards(
+ [request.GET['wildcard'],]
+ )
+ count = matching_tags.count()
+ names = matching_tags.values_list('name', flat = True)[:10]
+ re_data = simplejson.dumps({'tag_count': count, 'tag_names': list(names)})
+ return HttpResponse(re_data, mimetype = 'application/json')
+
+
+def subscribe_for_tags(request):
+ """process subscription of users by tags"""
+ tag_names = request.REQUEST['tags'].strip().split()
+ pure_tag_names, wildcards = forms.clean_marked_tagnames(tag_names)
+ if request.user.is_authenticated():
+ if request.method == 'POST':
+ if 'ok' in request.POST:
+ request.user.mark_tags(
+ pure_tag_names,
+ wildcards,
+ reason = 'good',
+ action = 'add'
+ )
+ request.user.message_set.create(
+ message = _('Your tag subscription was saved, thanks!')
+ )
+ else:
+ message = _(
+ 'Tag subscription was canceled (<a href="%(url)s">undo</a>).'
+ ) % {'url': request.path + '?tags=' + request.REQUEST['tags']}
+ request.user.message_set.create(message = message)
+ return HttpResponseRedirect(reverse('index'))
+ else:
+ data = {'tags': tag_names}
+ return render_into_skin('subscribe_for_tags.html', data, request)
+ else:
+ all_tag_names = pure_tag_names + wildcards
+ message = _('Please sign in to subscribe for: %(tags)s') \
+ % {'tags': ', '.join(all_tag_names)}
+ request.user.message_set.create(message = message)
+ request.session['subscribe_for_tags'] = (pure_tag_names, wildcards)
+ return HttpResponseRedirect(reverse('user_signin'))
+
-@ajax_login_required
+@decorators.ajax_login_required
def ajax_toggle_ignored_questions(request):#ajax tagging and tag-filtering system
if request.user.hide_ignored_questions:
new_hide_setting = False
@@ -367,7 +419,7 @@ def ajax_toggle_ignored_questions(request):#ajax tagging and tag-filtering syste
request.user.hide_ignored_questions = new_hide_setting
request.user.save()
-@ajax_only
+@decorators.ajax_only
def ajax_command(request):
"""view processing ajax commands - note "vote" and view others do it too
"""
@@ -384,7 +436,7 @@ def close(request, id):#close question
question = get_object_or_404(models.Question, id=id)
try:
if request.method == 'POST':
- form = CloseForm(request.POST)
+ form = forms.CloseForm(request.POST)
if form.is_valid():
reason = form.cleaned_data['reason']
@@ -395,7 +447,7 @@ def close(request, id):#close question
return HttpResponseRedirect(question.get_absolute_url())
else:
request.user.assert_can_close_question(question)
- form = CloseForm()
+ form = forms.CloseForm()
data = {
'question': question,
'form': form,