diff options
22 files changed, 830 insertions, 107 deletions
diff --git a/askbot/deps/django_authopenid/forms.py b/askbot/deps/django_authopenid/forms.py index 4ce7242b..8328da4d 100644 --- a/askbot/deps/django_authopenid/forms.py +++ b/askbot/deps/django_authopenid/forms.py @@ -394,7 +394,7 @@ class AccountRecoveryForm(forms.Form): try: user = User.objects.filter(email__iexact=email)[0] self.cleaned_data['user'] = user - except KeyError: + except IndexError: del self.cleaned_data['email'] message = _('Sorry, we don\'t have this email address in the database') raise forms.ValidationError(message) diff --git a/askbot/deps/django_authopenid/views.py b/askbot/deps/django_authopenid/views.py index bea67d2f..8f6ee101 100644 --- a/askbot/deps/django_authopenid/views.py +++ b/askbot/deps/django_authopenid/views.py @@ -1256,6 +1256,7 @@ def send_email_key(email, key, handler_url_name='user_account_recover'): {'site': askbot_settings.APP_SHORT_NAME} data = { + 'site_name': askbot_settings.APP_SHORT_NAME, 'validation_link': site_url(reverse(handler_url_name)) + \ '?validation_code=' + key } diff --git a/askbot/doc/source/changelog.rst b/askbot/doc/source/changelog.rst index 48afb5ea..2e300278 100644 --- a/askbot/doc/source/changelog.rst +++ b/askbot/doc/source/changelog.rst @@ -3,6 +3,7 @@ Changes in Askbot Development version ------------------- +* Added backend support for the tag synonyms * Added management command `apply_hinted_tags` to batch-apply tags from a list * Added hovercard on the user's karma display in the header * Added option to hide ad blocks from logged in users diff --git a/askbot/doc/source/contributors.rst b/askbot/doc/source/contributors.rst index 85d252bc..04a93e40 100644 --- a/askbot/doc/source/contributors.rst +++ b/askbot/doc/source/contributors.rst @@ -48,6 +48,7 @@ Programming, bug fixes and documentation * `Zafer Cakmak <https://github.com/xaph>`_ * `Kevin Porterfield <http://www.shotgunsoftware.com>_` * `Robert Martin <https://github.com/bobbydavid>_` +* `pcompassion https://github.com/pcompassion`_ Translations ------------ diff --git a/askbot/doc/source/management-commands.rst b/askbot/doc/source/management-commands.rst index da93dcb5..a56aa47a 100644 --- a/askbot/doc/source/management-commands.rst +++ b/askbot/doc/source/management-commands.rst @@ -30,6 +30,11 @@ The bulk of the management commands fall into this group and will probably be th | | one per line. If many tags match - only the most frequent | | | will be selected. | +---------------------------------+-------------------------------------------------------------+ +| `create_tag_synonyms --from | Creates tag synonym record from one name to another, | +| <from_name> --to <to_name> | creates the tag named as given with the value of `--to` | +| --user-id <user_id>` | if not existing, and the tag creator will be user with id | +| | equal to the value of `--user-id` parameter. | ++---------------------------------+-------------------------------------------------------------+ | `remove_admin <user_id>` | Remove admin status from a user account - the opposite of | | | the `add_admin` command | +---------------------------------+-------------------------------------------------------------+ diff --git a/askbot/management/commands/create_tag_synonyms.py b/askbot/management/commands/create_tag_synonyms.py new file mode 100644 index 00000000..e4324639 --- /dev/null +++ b/askbot/management/commands/create_tag_synonyms.py @@ -0,0 +1,147 @@ +"""management command that creates a tag synonym +all corresponding questions are retagged +""" + +import sys +from optparse import make_option +from django.core import management +from django.core.management.base import BaseCommand, CommandError +from askbot import models +from askbot.management.commands.rename_tags import get_admin +from askbot.utils import console +import datetime + + + +def decode_input(input): + decoded_input = input.decode(sys.stdin.encoding) + decoded_input = decoded_input.strip() + return decoded_input + + +class Command(BaseCommand): + + help = """create TagSynonym, +retags questions from source_tag_name to target_tag_name, +remove source_tag""" + + option_list = BaseCommand.option_list + ( + make_option('--from', + action = 'store', + type = 'str', + dest = 'from', + default = None, + help = 'a source tag name which needs to be replaced' + ), + make_option('--to', + action = 'store', + type = 'str', + dest = 'to', + default = None, + help = 'a target tag name that are to be used instead' + ), + make_option('--user-id', + action = 'store', + type = 'int', + dest = 'user_id', + default = None, + help = 'id of the user who will be marked as a performer of this operation' + ), + ) + + def handle(self, *args, **options): + """command handle function. reads tag names, decodes + them using the standard input encoding and attempts to find + the matching tags + + If "from" tag is not resolved, command fails + if "to" tag is not resolved, a new tag is created + """ + + if options['from'] is None: + raise CommandError('the --from argument is required') + if options['to'] is None: + raise CommandError('the --to argument is required') + + source_tag_name = decode_input(options['from']) + target_tag_name = decode_input(options['to']) + + if source_tag_name == target_tag_name: + raise CommandError("source and target tags appear to be the same") + + admin = get_admin(seed_user_id = options['user_id']) + + source_tag = None + is_source_tag_created = False + + try: + source_tag = models.Tag.objects.get(name=source_tag_name) + except models.Tag.DoesNotExist: + if not options.get('is_force', False): + prompt = """source tag %s doesn't exist, are you sure you want to create a TagSynonym + %s ==> %s?""" % (source_tag_name, source_tag_name, target_tag_name) + choice = console.choice_dialog(prompt, choices=('yes', 'no')) + if choice == 'no': + print 'Cancled' + sys.exit() + source_tag = models.Tag.objects.create(name=source_tag_name, + created_by=admin + ) + is_source_tag_created = True + + + # test if target_tag is actually synonym for yet another tag + # when user asked tag2->tag3, we already had tag3->tag4. + try: + tag_synonym_tmp = models.TagSynonym.objects.get(source_tag_name = target_tag_name) + if not options.get('is_force', False): + prompt = """There exists a TagSynonym %s ==> %s, + hence we will create a tag synonym %s ==> %s instead. Proceed?""" % (tag_synonym_tmp.source_tag_name, tag_synonym_tmp.target_tag_name, + source_tag_name, tag_synonym_tmp.target_tag_name) + choice = console.choice_dialog(prompt, choices=('yes', 'no')) + if choice == 'no': + print 'Cancled' + sys.exit() + target_tag_name = tag_synonym_tmp.target_tag_name + options['to'] = target_tag_name + except models.TagSynonym.DoesNotExist: + pass + + try: + models.Tag.objects.get(name=target_tag_name) + except models.Tag.DoesNotExist: + # we are creating a target tag, let's copy source tag's info + # used_count are updated later + models.Tag.objects.create(name=target_tag_name, + created_by = admin, + status = source_tag.status, + tag_wiki = source_tag.tag_wiki + ) + + tag_synonym_tmp, created = models.TagSynonym.objects.get_or_create(source_tag_name = source_tag_name, + target_tag_name = target_tag_name, + owned_by = admin + ) + + management.call_command('rename_tags', *args, **options) + + # When source_tag_name is a target_tag_name of already existing TagSynonym. + # ie. if tag1->tag2 exists when user asked tag2->tag3 + # we are going to convert all tag1->tag2 to tag1->tag3 as well + existing_tag_synonyms = models.TagSynonym.objects.filter(target_tag_name=source_tag_name) + for existing_tag_synonym in existing_tag_synonyms: + new_options = options.copy() + new_options['from'] = existing_tag_synonym.source_tag_name + new_options['user_id'] = admin.id + new_options['is_force'] = True # this is mandatory conversion + new_options['timestamp'] = datetime.datetime.now() + existing_tag_synonym.delete() # no longer needed + self.handle(*args, **new_options) + + # delete source Tag + if is_source_tag_created: + source_tag.delete() + else: + source_tag.deleted = True + source_tag.deleted_at = options.get('timestamp', datetime.datetime.now()) + source_tag.deleted_by = admin diff --git a/askbot/management/commands/rename_tags_id.py b/askbot/management/commands/rename_tags_id.py index f6fe05ae..1da22868 100644 --- a/askbot/management/commands/rename_tags_id.py +++ b/askbot/management/commands/rename_tags_id.py @@ -123,21 +123,33 @@ or repost a bug, if that does not help""" for question in questions[:10]: print '* %s' % question.title.strip() - from_tag_names = format_tag_name_list(from_tags) - to_tag_names = format_tag_name_list(to_tags) - - prompt = 'Rename tags %s --> %s?' % (from_tag_names, to_tag_names) - choice = console.choice_dialog(prompt, choices=('yes', 'no')) - if choice == 'no': - print 'Canceled' - sys.exit() + formatted_from_tag_names = format_tag_name_list(from_tags) + formatted_to_tag_names = format_tag_name_list(to_tags) + + if not options.get('is_force', False): + prompt = 'Rename tags %s --> %s?' % (formatted_from_tag_names, formatted_to_tag_names) + choice = console.choice_dialog(prompt, choices=('yes', 'no')) + if choice == 'no': + print 'Canceled' + sys.exit() else: - sys.stdout.write('Processing:') + print 'Renaming tags %s --> %s' % (formatted_from_tag_names, formatted_to_tag_names) + sys.stdout.write('Processing:') - #actual processing stage, only after this point we start to - #modify stuff in the database, one question per transaction from_tag_names = get_tag_names(from_tags) to_tag_names = get_tag_names(to_tags) + + #if user provided tag1 as to_tag, and tagsynonym tag1->tag2 exists. + for to_tag_name in to_tag_names: + try: + tag_synonym = models.TagSynonym.objects.get(source_tag_name = to_tag_name) + raise CommandError(u'You gave %s as --to argument, but TagSynonym: %s -> %s exists, probably you want to provide %s as --to argument' % (to_tag_name, tag_synonym.source_tag_name, tag_synonym.target_tag_name, tag_synonym.target_tag_name)) + except models.TagSynonym.DoesNotExist: + pass + + + #actual processing stage, only after this point we start to + #modify stuff in the database, one question per transaction i = 0 for question in questions: tag_names = set(question.get_tag_names()) @@ -174,3 +186,9 @@ or repost a bug, if that does not help""" # print "None found." #print "Done." #transaction.commit() + + # A user wants to rename tag2->tag3 and tagsynonym tag1->tag2 exists. + # we want to update tagsynonym (tag1->tag2) to (tag1->tag3) + for from_tag_name in from_tag_names: + # we need db_index for target_tag_name as well for this + models.TagSynonym.objects.filter(target_tag_name = from_tag_name).update(target_tag_name = to_tag_name) diff --git a/askbot/management/commands/send_email_alerts.py b/askbot/management/commands/send_email_alerts.py index b7432296..06f40c65 100644 --- a/askbot/management/commands/send_email_alerts.py +++ b/askbot/management/commands/send_email_alerts.py @@ -29,7 +29,8 @@ def get_all_origin_posts(mentions): def extend_question_list( src, dst, cutoff_time = None, limit=False, add_mention=False, - add_comment = False + add_comment = False, + languages=None ): """src is a query set with questions or None @@ -48,6 +49,8 @@ def extend_question_list( raise ValueError('cutoff_time is a mandatory parameter') for q in src: + if languages and src.language_code not in languages: + continue if q in dst: meta_data = dst[q] else: @@ -162,6 +165,11 @@ class Command(NoArgsCommand): Q_set_A = not_seen_qs Q_set_B = seen_before_last_mod_qs + if django_settings.ASKBOT_MULTILINGUAL: + languages = user.languages.split() + else: + languages = None + for feed in user_feeds: if feed.feed_type == 'm_and_c': #alerts on mentions and comments are processed separately @@ -216,8 +224,8 @@ class Command(NoArgsCommand): q_list = SortedDict() #todo: refactor q_list into a separate class? - extend_question_list(q_sel_A, q_list) - extend_question_list(q_sel_B, q_list) + extend_question_list(q_sel_A, q_list, languages=languages) + extend_question_list(q_sel_B, q_list, languages=languages) #build list of comment and mention responses here #it is separate because posts are not marked as changed @@ -247,8 +255,9 @@ class Command(NoArgsCommand): extend_question_list( q_commented, q_list, - cutoff_time = cutoff_time, - add_comment = True + cutoff_time=cutoff_time, + add_comment=True, + languages=languages ) mentions = Activity.objects.get_mentions( @@ -267,27 +276,37 @@ class Command(NoArgsCommand): q_mentions_A = Q_set_A.filter(id__in = q_mentions_id) q_mentions_A.cutoff_time = cutoff_time - extend_question_list(q_mentions_A, q_list, add_mention=True) + extend_question_list( + q_mentions_A, + q_list, + add_mention=True, + languages=languages + ) q_mentions_B = Q_set_B.filter(id__in = q_mentions_id) q_mentions_B.cutoff_time = cutoff_time - extend_question_list(q_mentions_B, q_list, add_mention=True) + extend_question_list( + q_mentions_B, + q_list, + add_mention=True, + languages=languages + ) except EmailFeedSetting.DoesNotExist: pass if user.email_tag_filter_strategy == const.INCLUDE_INTERESTING: - extend_question_list(q_all_A, q_list) - extend_question_list(q_all_B, q_list) + extend_question_list(q_all_A, q_list, languages=languages) + extend_question_list(q_all_B, q_list, languages=languages) - extend_question_list(q_ask_A, q_list, limit=True) - extend_question_list(q_ask_B, q_list, limit=True) + extend_question_list(q_ask_A, q_list, limit=True, languages=languages) + extend_question_list(q_ask_B, q_list, limit=True, languages=languages) - extend_question_list(q_ans_A, q_list, limit=True) - extend_question_list(q_ans_B, q_list, limit=True) + extend_question_list(q_ans_A, q_list, limit=True, languages=languages) + extend_question_list(q_ans_B, q_list, limit=True, languages=languages) if user.email_tag_filter_strategy == const.EXCLUDE_IGNORED: - extend_question_list(q_all_A, q_list, limit=True) - extend_question_list(q_all_B, q_list, limit=True) + extend_question_list(q_all_A, q_list, limit=True, languages=languages) + extend_question_list(q_all_B, q_list, limit=True, languages=languages) ctype = ContentType.objects.get_for_model(Post) EMAIL_UPDATE_ACTIVITY = const.TYPE_ACTIVITY_EMAIL_UPDATE_SENT diff --git a/askbot/media/js/user.js b/askbot/media/js/user.js index 957bfb4c..b5a999a5 100644 --- a/askbot/media/js/user.js +++ b/askbot/media/js/user.js @@ -804,7 +804,7 @@ UserGroup.prototype.decorate = function(element){ this._name = $.trim(element.find('a').html()); var deleter = new DeleteIcon(); deleter.setHandler(this.getDeleteHandler()); - deleter.setContent(gettext('Remove')); + //deleter.setContent(gettext('Remove')); this._element.find('td:last').append(deleter.getElement()); this._delete_icon = deleter; }; @@ -946,6 +946,7 @@ GroupAdderWidget.prototype.toggleState = function(){ GroupAdderWidget.prototype.decorate = function(element){ this._element = element; var input = this.makeElement('input'); + input.attr('type', 'text'); this._input = input; var groupsAc = new AutoCompleter({ diff --git a/askbot/media/style/style.css b/askbot/media/style/style.css index fbbd206a..55769a17 100644 --- a/askbot/media/style/style.css +++ b/askbot/media/style/style.css @@ -3013,6 +3013,7 @@ a:hover.medal { } .user-details td { padding-right: 10px; + vertical-align: top; } .user-about { background-color: #EEEEEE; @@ -3852,6 +3853,9 @@ p.signup_p { padding: 0; margin-top: -3px; } +.user-profile-page .vote-notification h3 { + padding: 10px; +} .user-profile-page ul.tags { margin-left: 5px; } @@ -4006,24 +4010,23 @@ body.anon.lang-es #searchBar { } /* user groups */ +#user-groups input[type="text"] { + height: 21px; + width: 100px; +} #user-groups ul { margin-bottom: 0px; } #user-groups .delete-icon { - float: none; - display: inline; - color: #525252; - padding: 0 3px 0 3px; - background: #ccc; - border-radius: 4px; - line-height: inherit; - -moz-border-radius: 4px; - -khtml-border-radius: 4px; - -webkit-border-radius: 4px; + background: url(../images/close-small.png) no-repeat; + border: none; + display: inline-block; + height: 14px; + margin-top: 4px; + width: 14px; } #user-groups .delete-icon:hover { - color: white; - background: #b32f2f; + background: url(../images/close-small-hover.png) no-repeat; } .question-page .post-update-info a.primary-group-name, a.primary-group-name { @@ -4058,9 +4061,6 @@ img.group-logo { #groups-list .group-name { padding-right: 20px; } -#groups-list td { - padding-bottom: 5px; -} .groups-page #groups-list th, .groups-page #groups-list td { padding-right: 20px; diff --git a/askbot/media/style/style.less b/askbot/media/style/style.less index 4a957dd6..83be895f 100644 --- a/askbot/media/style/style.less +++ b/askbot/media/style/style.less @@ -3133,6 +3133,7 @@ a:hover.medal { } td { padding-right: 10px; + vertical-align: top; } } @@ -4099,6 +4100,9 @@ p.signup_p { padding: 0; margin-top: -3px; } + .vote-notification h3 { + padding: 10px; + } ul.tags { margin-left: 5px; } @@ -4169,24 +4173,25 @@ body.anon.lang-es { } /* user groups */ -#user-groups ul { - margin-bottom: 0px; -} -#user-groups .delete-icon { - float: none; - display: inline; - color: #525252; - padding: 0 3px 0 3px; - background: #ccc; - border-radius: 4px; - line-height:inherit; - -moz-border-radius: 4px; - -khtml-border-radius: 4px; - -webkit-border-radius: 4px; -} -#user-groups .delete-icon:hover { - color: white; - background: #b32f2f; +#user-groups { + input[type="text"] { + height: 21px; + width: 100px; + } + ul { + margin-bottom: 0px; + } + .delete-icon { + background: url(../images/close-small.png) no-repeat; + border: none; + display: inline-block; + height: 14px; + margin-top: 4px; + width: 14px; + } + .delete-icon:hover { + background: url(../images/close-small-hover.png) no-repeat; + } } .question-page .post-update-info a.primary-group-name, @@ -4229,9 +4234,6 @@ img.group-logo { .group-name { padding-right: 20px; } - td { - padding-bottom: 5px; - } } .groups-page #groups-list { diff --git a/askbot/migrations/0169_auto__add_tagsynonym.py b/askbot/migrations/0169_auto__add_tagsynonym.py new file mode 100644 index 00000000..bd217056 --- /dev/null +++ b/askbot/migrations/0169_auto__add_tagsynonym.py @@ -0,0 +1,413 @@ +# -*- coding: 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): + # Adding model 'TagSynonym' + db.create_table('askbot_tagsynonym', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('source_tag_name', self.gf('django.db.models.fields.CharField')(unique=True, max_length=255)), + ('target_tag_name', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('created_at', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), + ('owned_by', self.gf('django.db.models.fields.related.ForeignKey')(related_name='tag_synonyms', to=orm['auth.User'])), + ('auto_rename_count', self.gf('django.db.models.fields.IntegerField')(default=0)), + ('last_auto_rename_at', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)), + )) + db.send_create_signal('askbot', ['TagSynonym']) + + + def backwards(self, orm): + # Deleting model 'TagSynonym' + db.delete_table('askbot_tagsynonym') + + + 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'}), + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Post']", 'null': 'True'}), + 'receiving_users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'received_activity'", 'symmetrical': 'False', 'to': "orm['auth.User']"}), + 'recipients': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'incoming_activity'", 'symmetrical': 'False', 'through': "orm['askbot.ActivityAuditStatus']", 'to': "orm['auth.User']"}), + 'summary': ('django.db.models.fields.TextField', [], {'default': "''"}), + '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.Post']"}), + 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'text': ('django.db.models.fields.TextField', [], {}), + 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + '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'}), + 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + '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'}) + }, + 'askbot.askwidget': { + 'Meta': {'object_name': 'AskWidget'}, + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']", 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'include_text_field': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'inner_style': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'outer_style': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'tag': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Tag']", 'null': 'True', 'blank': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + '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'}), + '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': {'ordering': "('slug',)", 'object_name': 'BadgeData'}, + 'awarded_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'awarded_to': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'badges'", 'symmetrical': 'False', 'through': "orm['askbot.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'}) + }, + 'askbot.bulktagsubscription': { + 'Meta': {'ordering': "['-date_added']", 'object_name': 'BulkTagSubscription'}, + 'date_added': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['askbot.Group']", 'symmetrical': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['askbot.Tag']", 'symmetrical': 'False'}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'symmetrical': 'False'}) + }, + 'askbot.draftanswer': { + 'Meta': {'object_name': 'DraftAnswer'}, + 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'draft_answers'", 'to': "orm['auth.User']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'text': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'thread': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'draft_answers'", 'to': "orm['askbot.Thread']"}) + }, + 'askbot.draftquestion': { + 'Meta': {'object_name': 'DraftQuestion'}, + 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125', 'null': 'True'}), + 'text': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '300', 'null': 'True'}) + }, + 'askbot.emailfeedsetting': { + 'Meta': {'unique_together': "(('subscriber', 'feed_type'),)", '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'}), + 'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Thread']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'user_favorite_questions'", 'to': "orm['auth.User']"}) + }, + 'askbot.group': { + 'Meta': {'object_name': 'Group', '_ormbases': ['auth.Group']}, + 'description': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'described_group'", 'unique': 'True', 'null': 'True', 'to': "orm['askbot.Post']"}), + 'group_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.Group']", 'unique': 'True', 'primary_key': 'True'}), + 'is_vip': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'logo_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True'}), + 'moderate_answers_to_enquirers': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'moderate_email': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'openness': ('django.db.models.fields.SmallIntegerField', [], {'default': '2'}), + 'preapproved_email_domains': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}), + 'preapproved_emails': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}) + }, + 'askbot.groupmembership': { + 'Meta': {'object_name': 'GroupMembership', '_ormbases': ['auth.AuthUserGroups']}, + 'authusergroups_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.AuthUserGroups']", 'unique': 'True', 'primary_key': 'True'}), + 'level': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}) + }, + '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.post': { + 'Meta': {'object_name': 'Post'}, + 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'approved': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}), + 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'posts'", 'to': "orm['auth.User']"}), + 'comment_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': '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_posts'", 'null': 'True', 'to': "orm['auth.User']"}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'group_posts'", 'symmetrical': 'False', 'through': "orm['askbot.PostToGroup']", 'to': "orm['askbot.Group']"}), + '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'}), + 'language_code': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '16'}), + '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_posts'", 'null': 'True', 'to': "orm['auth.User']"}), + 'locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'locked_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'locked_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'locked_posts'", 'null': 'True', 'to': "orm['auth.User']"}), + 'offensive_flag_count': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'old_answer_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'old_comment_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'old_question_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'comments'", 'null': 'True', 'to': "orm['askbot.Post']"}), + 'points': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_column': "'score'"}), + 'post_type': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'summary': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'text': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'thread': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'posts'", 'null': 'True', 'blank': 'True', 'to': "orm['askbot.Thread']"}), + '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'}), + 'wikified_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + 'askbot.postflagreason': { + 'Meta': {'object_name': 'PostFlagReason'}, + 'added_at': ('django.db.models.fields.DateTimeField', [], {}), + 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), + 'details': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'post_reject_reasons'", 'to': "orm['askbot.Post']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'askbot.postrevision': { + 'Meta': {'ordering': "('-revision',)", 'unique_together': "(('post', 'revision'),)", 'object_name': 'PostRevision'}, + 'approved': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'approved_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'approved_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'postrevisions'", 'to': "orm['auth.User']"}), + 'by_email': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'email_address': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'revisions'", 'null': 'True', 'to': "orm['askbot.Post']"}), + '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', [], {'default': "''", 'max_length': '125', 'blank': 'True'}), + 'text': ('django.db.models.fields.TextField', [], {}), + 'title': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '300', 'blank': 'True'}) + }, + 'askbot.posttogroup': { + 'Meta': {'unique_together': "(('post', 'group'),)", 'object_name': 'PostToGroup', 'db_table': "'askbot_post_groups'"}, + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Post']"}) + }, + '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.Post']"}), + 'when': ('django.db.models.fields.DateTimeField', [], {}), + 'who': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'question_views'", 'to': "orm['auth.User']"}) + }, + 'askbot.questionwidget': { + 'Meta': {'object_name': 'QuestionWidget'}, + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']", 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order_by': ('django.db.models.fields.CharField', [], {'default': "'-added_at'", 'max_length': '18'}), + 'question_number': ('django.db.models.fields.PositiveIntegerField', [], {'default': '7'}), + 'search_query': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '50', 'null': 'True', 'blank': 'True'}), + 'style': ('django.db.models.fields.TextField', [], {'default': '"\\n@import url(\'http://fonts.googleapis.com/css?family=Yanone+Kaffeesatz:300,400,700\');\\nbody {\\n overflow: hidden;\\n}\\n\\n#container {\\n width: 200px;\\n height: 350px;\\n}\\nul {\\n list-style: none;\\n padding: 5px;\\n margin: 5px;\\n}\\nli {\\n border-bottom: #CCC 1px solid;\\n padding-bottom: 5px;\\n padding-top: 5px;\\n}\\nli:last-child {\\n border: none;\\n}\\na {\\n text-decoration: none;\\n color: #464646;\\n font-family: \'Yanone Kaffeesatz\', sans-serif;\\n font-size: 15px;\\n}\\n"', 'blank': 'True'}), + 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'askbot.replyaddress': { + 'Meta': {'object_name': 'ReplyAddress'}, + 'address': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '25'}), + 'allowed_from_email': ('django.db.models.fields.EmailField', [], {'max_length': '150'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'reply_addresses'", 'null': 'True', 'to': "orm['askbot.Post']"}), + 'reply_action': ('django.db.models.fields.CharField', [], {'default': "'auto_answer_or_comment'", 'max_length': '32'}), + 'response_post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'edit_addresses'", 'null': 'True', 'to': "orm['askbot.Post']"}), + 'used_at': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'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.Post']", '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': {'ordering': "('-used_count', 'name')", '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'}), + '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'}), + 'status': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}), + 'suggested_by': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'suggested_tags'", 'symmetrical': 'False', 'to': "orm['auth.User']"}), + 'tag_wiki': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'described_tag'", 'unique': 'True', 'null': 'True', 'to': "orm['askbot.Post']"}), + 'used_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}) + }, + 'askbot.tagsynonym': { + 'Meta': {'object_name': 'TagSynonym'}, + 'auto_rename_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_auto_rename_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'owned_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tag_synonyms'", 'to': "orm['auth.User']"}), + 'source_tag_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'target_tag_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'askbot.thread': { + 'Meta': {'object_name': 'Thread'}, + 'accepted_answer': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['askbot.Post']"}), + 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'answer_accepted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'answer_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'approved': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}), + 'close_reason': ('django.db.models.fields.SmallIntegerField', [], {'null': 'True', 'blank': 'True'}), + 'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'closed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'closed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'favorited_by': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'unused_favorite_threads'", 'symmetrical': 'False', 'through': "orm['askbot.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_threads'", 'symmetrical': 'False', 'to': "orm['auth.User']"}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'group_threads'", 'symmetrical': 'False', 'through': "orm['askbot.ThreadToGroup']", 'to': "orm['askbot.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'language_code': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '16'}), + 'last_activity_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_activity_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'unused_last_active_in_threads'", 'to': "orm['auth.User']"}), + 'points': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_column': "'score'"}), + 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125'}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'threads'", 'symmetrical': 'False', 'to': "orm['askbot.Tag']"}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '300'}), + 'view_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}) + }, + 'askbot.threadtogroup': { + 'Meta': {'unique_together': "(('thread', 'group'),)", 'object_name': 'ThreadToGroup', 'db_table': "'askbot_thread_groups'"}, + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Thread']"}), + 'visibility': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}) + }, + 'askbot.vote': { + 'Meta': {'unique_together': "(('user', 'voted_post'),)", 'object_name': 'Vote', 'db_table': "u'vote'"}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + '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'}), + 'voted_post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'votes'", 'to': "orm['askbot.Post']"}) + }, + 'auth.authusergroups': { + 'Meta': {'unique_together': "(('group', 'user'),)", 'object_name': 'AuthUserGroups', 'db_table': "'auth_user_groups'", 'managed': 'False'}, + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + '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']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", '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'}), + 'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}), + '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'}), + 'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'email_signature': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}), + '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']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_fake': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'languages': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '128'}), + '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'}), + 'show_marked_tags': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'social_sharing_mode': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}), + 'subscribed_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'twitter_access_token': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256'}), + 'twitter_handle': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", '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']
\ No newline at end of file diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py index 78a68487..797aa6a3 100644 --- a/askbot/models/__init__.py +++ b/askbot/models/__init__.py @@ -42,7 +42,7 @@ from askbot.mail import messages from askbot.models.question import QuestionView, AnonymousQuestion from askbot.models.question import DraftQuestion from askbot.models.question import FavoriteQuestion -from askbot.models.tag import Tag, MarkedTag +from askbot.models.tag import Tag, MarkedTag, TagSynonym from askbot.models.tag import format_personal_group_name from askbot.models.user import EmailFeedSetting, ActivityAuditStatus, Activity from askbot.models.user import GroupMembership @@ -3781,6 +3781,7 @@ __all__ = [ 'Vote', 'PostFlagReason', 'MarkedTag', + 'TagSynonym', 'BadgeData', 'Award', diff --git a/askbot/models/question.py b/askbot/models/question.py index ed30c5f5..0f4c4a85 100644 --- a/askbot/models/question.py +++ b/askbot/models/question.py @@ -19,7 +19,7 @@ import askbot from askbot.conf import settings as askbot_settings from askbot import mail from askbot.mail import messages -from askbot.models.tag import Tag +from askbot.models.tag import Tag, TagSynonym from askbot.models.tag import get_tags_by_names from askbot.models.tag import filter_accepted_tags, filter_suggested_tags from askbot.models.tag import separate_unused_tags @@ -1264,6 +1264,8 @@ class Thread(models.Model): Tag use counts are recalculated A signal tags updated is sent + TagSynonym is used to replace tag names + *IMPORTANT*: self._question_post() has to exist when update_tags() is called! """ @@ -1273,9 +1275,20 @@ class Thread(models.Model): previous_tags = list(self.tags.filter(status = Tag.STATUS_ACCEPTED)) ordered_updated_tagnames = [t for t in tagnames.strip().split(' ')] + updated_tagnames_tmp = set(ordered_updated_tagnames) + + #apply TagSynonym + updated_tagnames = set() + for tag_name in updated_tagnames_tmp: + try: + tag_synonym = TagSynonym.objects.get(source_tag_name=tag_name) + updated_tagnames.add(tag_synonym.target_tag_name) + tag_synonym.auto_rename_count += 1 + tag_synonym.save() + except TagSynonym.DoesNotExist: + updated_tagnames.add(tag_name) previous_tagnames = set([tag.name for tag in previous_tags]) - updated_tagnames = set(ordered_updated_tagnames) removed_tagnames = previous_tagnames - updated_tagnames #remove tags from the question's tags many2many relation diff --git a/askbot/models/tag.py b/askbot/models/tag.py index 455995e0..bf054627 100644 --- a/askbot/models/tag.py +++ b/askbot/models/tag.py @@ -318,3 +318,20 @@ class MarkedTag(models.Model): class Meta: app_label = 'askbot' + + +class TagSynonym(models.Model): + + source_tag_name = models.CharField(max_length=255, unique=True) + target_tag_name = models.CharField(max_length=255, db_index=True) + created_at = models.DateTimeField(auto_now_add=True) + owned_by = models.ForeignKey(User, related_name='tag_synonyms') + auto_rename_count = models.IntegerField(default=0) + last_auto_rename_at = models.DateTimeField(auto_now=True) + + class Meta: + app_label = 'askbot' + + def __unicode__(self): + return u'%s -> %s' % (self.source_tag_name, self.target_tag_name) + diff --git a/askbot/templates/authopenid/email_validation.html b/askbot/templates/authopenid/email_validation.html index 616b285f..4a74b23f 100644 --- a/askbot/templates/authopenid/email_validation.html +++ b/askbot/templates/authopenid/email_validation.html @@ -1,20 +1,20 @@ {% extends "email/base_mail.html"%} -{% block title %}{% trans %}Greetings from the Q&A forum{% endtrans %},{%endblock%} -{% block headline%}{% trans %}Greetings from the Q&A forum{% endtrans %},{%endblock%} +{% block title %}{% trans %}Greetings from the {{ site_name }}{% endtrans %},{%endblock%} +{% block headline %}{% trans %}Greetings from the {{ site_name }}{% endtrans %},{%endblock%} -{%block content%} +{% block content %} <p>{% trans %}To make use of the Forum, please follow the link below:{% endtrans %}</p> -<p><a href="{{validation_link}}" >{{validation_link}}</a></p> +<p><a href="{{ validation_link }}" >{{ validation_link }}</a></p> <p>{% trans %}Following the link above will help us verify your email address.{% endtrans %}</p> <p>{% trans %}If you believe that this message was sent in mistake - no further action is needed. Just ignore this email, we apologize for any inconvenience{% endtrans %}</p> -{%endblock%} +{% endblock %} -{%block footer %} +{% block footer %} {% include "email/footer.html" %} {% endblock %} diff --git a/askbot/templates/user_profile/user_info.html b/askbot/templates/user_profile/user_info.html index 95baad20..6c20f1f4 100644 --- a/askbot/templates/user_profile/user_info.html +++ b/askbot/templates/user_profile/user_info.html @@ -58,19 +58,21 @@ {% endif %} {% if settings.GROUPS_ENABLED %} <tr> - <td>{% if user_groups %}{% trans %}groups{% endtrans %}{% endif %}</td> + <td>{% trans %}groups{% endtrans %}</td> <td> - <table id="groups-list"> - {% for group in user_groups %} - <tr> - {{ macros.user_group(group, groups_membership_info[group.id]) }} - </tr> - {% endfor %} - </table> - <div class="clearfix"></div> - <a id="add-group">{% trans %}add group{% endtrans %}</a> + <div id="user-groups"> + <table id="groups-list"> + {% for group in user_groups %} + <tr> + {{ macros.user_group(group, groups_membership_info[group.id]) }} + </tr> + {% endfor %} + </table> + <div class="clearfix"></div> + <a id="add-group">{% trans %}add group{% endtrans %}</a> + </div> </td> - </div> + </tr> {% endif %} <tr> <td>{% trans %}member since{% endtrans %}</td> diff --git a/askbot/tests/cache_tests.py b/askbot/tests/cache_tests.py index 5740cc2a..e0703d08 100644 --- a/askbot/tests/cache_tests.py +++ b/askbot/tests/cache_tests.py @@ -11,16 +11,19 @@ class CacheTests(AskbotTestCase): self.post_answer(user=user, question=self.question) settings.DEBUG = True # because it's forsed to False + def tearDown(self): + settings.DEBUG = False + def visit_question(self): self.client.get(self.question.get_absolute_url(), follow=True) - + def test_anonymous_question_cache(self): self.visit_question() - counter = len(connection.queries) - print 'we have %d queries' % counter + before_count = len(connection.queries) self.visit_question() - #second hit to the same question should give fewer queries - self.assertTrue(counter > len(connection.queries)) - settings.DEBUG = False + after_count = len(connection.queries) + self.assertTrue(before_count > after_count, + ('Expected fewer queries after calling visit_question. ' + + 'Before visit: %d. After visit: %d.') % (before_count, after_count)) diff --git a/askbot/tests/email_parsing_tests.py b/askbot/tests/email_parsing_tests.py index 3ed0908a..9a5ff126 100644 --- a/askbot/tests/email_parsing_tests.py +++ b/askbot/tests/email_parsing_tests.py @@ -18,12 +18,7 @@ class EmailParsingTests(utils.AskbotTestCase): def test_clean_email_body(self): cleaned_body = mail.clean_html_email(self.rendered_template) - print "EXPECTED BODY" - print self.expected_output - print '==================================================' - print cleaned_body - print "CLEANED BODY" - self.assertEqual(cleaned_body, self.expected_output) + self.assertEqual(self.expected_output, cleaned_body) def test_gmail_rich_text_response_stripped(self): text = u'\n\nthis is my reply!\n\nOn Wed, Oct 31, 2012 at 1:45 AM, <kp@kp-dev.askbot.com> wrote:\n\n> **\n> ' diff --git a/askbot/tests/management_command_tests.py b/askbot/tests/management_command_tests.py index f1c6728d..a44bb792 100644 --- a/askbot/tests/management_command_tests.py +++ b/askbot/tests/management_command_tests.py @@ -2,6 +2,7 @@ from django.core import management from django.contrib import auth from askbot.tests.utils import AskbotTestCase from askbot import models +from django.contrib.auth.models import User class ManagementCommandTests(AskbotTestCase): def test_add_askbot_user(self): @@ -50,3 +51,80 @@ class ManagementCommandTests(AskbotTestCase): user_two = models.User.objects.get(pk=user_two_pk) self.assertEqual(user_two.gold, number_of_gold) self.assertEqual(user_two.reputation, reputation) + + def test_create_tag_synonym(self): + + admin = User.objects.create_superuser('test_admin', 'admin@admin.com', 'admin_pass') + + options = { + 'from': 'tag1', # ok.. 'from' is a bad keyword argument name.. + 'to': 'tag2', + 'user_id': admin.id, + 'is_force': True + } + management.call_command( + 'create_tag_synonyms', + **options + ) + + options['from'] = 'tag3' + options['to'] = 'tag4' + management.call_command( + 'create_tag_synonyms', + **options + ) + + options['from']='tag5' + options['to']='tag4' + management.call_command( + 'create_tag_synonyms', + **options + ) + + options['from']='tag2' + options['to']='tag3' + management.call_command( + 'create_tag_synonyms', + **options + ) + + self.assertEqual(models.TagSynonym.objects.filter(source_tag_name = 'tag1', + target_tag_name = 'tag4' + ).count(), 1) + self.assertEqual(models.TagSynonym.objects.filter(source_tag_name = 'tag2', + target_tag_name = 'tag4' + ).count(), 1) + self.assertEqual(models.TagSynonym.objects.filter(source_tag_name = 'tag3', + target_tag_name = 'tag4' + ).count(), 1) + self.assertEqual(models.TagSynonym.objects.filter(source_tag_name = 'tag5', + target_tag_name = 'tag4' + ).count(), 1) + self.assertEqual(models.TagSynonym.objects.count(), 4) + + options['from']='tag4' + options['to']='tag6' + management.call_command( + 'create_tag_synonyms', + **options + ) + + self.assertEqual(models.TagSynonym.objects.filter(source_tag_name = 'tag1', + target_tag_name = 'tag6' + ).count(), 1) + self.assertEqual(models.TagSynonym.objects.filter(source_tag_name = 'tag2', + target_tag_name = 'tag6' + ).count(), 1) + self.assertEqual(models.TagSynonym.objects.filter(source_tag_name = 'tag3', + target_tag_name = 'tag6' + ).count(), 1) + self.assertEqual(models.TagSynonym.objects.filter(source_tag_name = 'tag4', + target_tag_name = 'tag6' + ).count(), 1) + self.assertEqual(models.TagSynonym.objects.filter(source_tag_name = 'tag5', + target_tag_name = 'tag6' + ).count(), 1) + self.assertEqual(models.TagSynonym.objects.count(), 5) + + print 'done create_tag_synonym_test' + diff --git a/askbot/tests/post_model_tests.py b/askbot/tests/post_model_tests.py index 703603c3..2e785802 100644 --- a/askbot/tests/post_model_tests.py +++ b/askbot/tests/post_model_tests.py @@ -546,7 +546,8 @@ class ThreadRenderCacheUpdateTests(AskbotTestCase): }) self.assertEqual(2, Post.objects.count()) answer = Post.objects.get_answers()[0] - self.assertRedirects(response=response, expected_url=answer.get_absolute_url()) + expected_url=answer.get_absolute_url() + self.assertRedirects(response=response, expected_url=expected_url) thread = answer.thread self.assertEqual(1, thread.answer_count) diff --git a/askbot/views/commands.py b/askbot/views/commands.py index b2c8a788..d072cc00 100644 --- a/askbot/views/commands.py +++ b/askbot/views/commands.py @@ -1028,18 +1028,23 @@ def edit_group_membership(request): action = form.cleaned_data['action'] #warning: possible race condition if action == 'add': - group_params = {'name': group_name, 'user': user} - group = models.Group.objects.get_or_create(**group_params) - request.user.edit_group_membership(user, group, 'add') - template = get_template('widgets/group_snippet.html') - return { - 'name': group.name, - 'description': getattr(group.tag_wiki, 'text', ''), - 'html': template.render({'group': group}) - } + try: + group = models.Group.objects.get(name=group_name) + request.user.edit_group_membership(user, group, 'add') + template = get_template('widgets/group_snippet.html') + return { + 'name': group.name, + 'description': getattr(group.description, 'text', ''), + 'html': template.render({'group': group}) + } + except models.Group.DoesNotExist: + raise exceptions.PermissionDenied( + _('Group %(name)s does not exist') % {'name': group_name} + ) + elif action == 'remove': try: - group = models.Group.objects.get(group_name = group_name) + group = models.Group.objects.get(name = group_name) request.user.edit_group_membership(user, group, 'remove') except models.Group.DoesNotExist: raise exceptions.PermissionDenied() |