diff options
author | Evgeny Fadeev <evgeny.fadeev@gmail.com> | 2011-02-28 14:08:00 -0500 |
---|---|---|
committer | Evgeny Fadeev <evgeny.fadeev@gmail.com> | 2011-02-28 14:08:00 -0500 |
commit | 04dd70280f2a9bcca38a52717031a2f35aa39910 (patch) | |
tree | afc3b227e67b15a3f6ce864357cf4ad301da039d | |
parent | 08bc5fa9271e710c7094bb19e2d9c7aa81aa8e2a (diff) | |
parent | 6cd71c8523ea8a24c4e05e079987c4e6883c82a1 (diff) | |
download | askbot-04dd70280f2a9bcca38a52717031a2f35aa39910.tar.gz askbot-04dd70280f2a9bcca38a52717031a2f35aa39910.tar.bz2 askbot-04dd70280f2a9bcca38a52717031a2f35aa39910.zip |
Merge branch 'andrei'
-rw-r--r-- | askbot/conf/forum_data_rules.py | 14 | ||||
-rw-r--r-- | askbot/deps/django_authopenid/views.py | 7 | ||||
-rw-r--r-- | askbot/forms.py | 25 | ||||
-rw-r--r-- | askbot/management/commands/send_email_alerts.py | 26 | ||||
-rw-r--r-- | askbot/migrations/0037_add_marked_tags_to_user_profile.py | 315 | ||||
-rw-r--r-- | askbot/models/__init__.py | 136 | ||||
-rw-r--r-- | askbot/models/content.py | 47 | ||||
-rw-r--r-- | askbot/models/question.py | 15 | ||||
-rw-r--r-- | askbot/models/tag.py | 21 | ||||
-rw-r--r-- | askbot/skins/default/media/js/live_search.js | 67 | ||||
-rw-r--r-- | askbot/skins/default/media/js/post.js | 93 | ||||
-rw-r--r-- | askbot/skins/default/media/js/tag_selector.js | 209 | ||||
-rw-r--r-- | askbot/skins/default/media/js/utils.js | 274 | ||||
-rwxr-xr-x | askbot/skins/default/media/style/style.css | 11 | ||||
-rw-r--r-- | askbot/skins/default/templates/blocks/bottom_scripts.html | 1 | ||||
-rw-r--r-- | askbot/skins/default/templates/macros.html | 9 | ||||
-rw-r--r-- | askbot/skins/default/templates/subscribe_for_tags.html | 19 | ||||
-rw-r--r-- | askbot/tests/db_api_tests.py | 72 | ||||
-rw-r--r-- | askbot/urls.py | 10 | ||||
-rw-r--r-- | askbot/views/commands.py | 116 |
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(/\*$/, '✽')); + $.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(/\*$/, '✽'); +}; + +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('*', '✽') }}</{% 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, |