From 66a327ce3c03c1704e8bbe53b42731f7ed2c780c Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Thu, 5 Jul 2012 02:47:11 -0400 Subject: created a page for tag moderation, at this point it is only listing the suggested tags --- askbot/migrations/0130_auto__add_suggestedtag.py | 357 ++++++++++++++++++++++ askbot/models/__init__.py | 57 ++-- askbot/models/post.py | 4 +- askbot/models/question.py | 26 +- askbot/models/tag.py | 65 ++++ askbot/skins/default/media/style/style.less | 21 +- askbot/skins/default/templates/moderate_tags.html | 39 +++ askbot/skins/default/templates/tags.html | 25 +- askbot/skins/default/templates/tags/header.html | 34 +++ askbot/urls.py | 5 + askbot/views/meta.py | 48 ++- askbot/views/readers.py | 31 +- 12 files changed, 636 insertions(+), 76 deletions(-) create mode 100644 askbot/migrations/0130_auto__add_suggestedtag.py create mode 100644 askbot/skins/default/templates/moderate_tags.html create mode 100644 askbot/skins/default/templates/tags/header.html diff --git a/askbot/migrations/0130_auto__add_suggestedtag.py b/askbot/migrations/0130_auto__add_suggestedtag.py new file mode 100644 index 00000000..d769c76d --- /dev/null +++ b/askbot/migrations/0130_auto__add_suggestedtag.py @@ -0,0 +1,357 @@ +# -*- 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 'SuggestedTag' + db.create_table('askbot_suggestedtag', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(unique=True, max_length=255)), + ('used_count', self.gf('django.db.models.fields.PositiveIntegerField')(default=1)), + ('thread_ids', self.gf('django.db.models.fields.TextField')()), + )) + db.send_create_signal('askbot', ['SuggestedTag']) + + # Adding M2M table for field threads on 'SuggestedTag' + db.create_table('askbot_suggestedtag_threads', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('suggestedtag', models.ForeignKey(orm['askbot.suggestedtag'], null=False)), + ('thread', models.ForeignKey(orm['askbot.thread'], null=False)) + )) + db.create_unique('askbot_suggestedtag_threads', ['suggestedtag_id', 'thread_id']) + + # Adding M2M table for field users on 'SuggestedTag' + db.create_table('askbot_suggestedtag_users', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('suggestedtag', models.ForeignKey(orm['askbot.suggestedtag'], null=False)), + ('user', models.ForeignKey(orm['auth.user'], null=False)) + )) + db.create_unique('askbot_suggestedtag_users', ['suggestedtag_id', 'user_id']) + + def backwards(self, orm): + # Deleting model 'SuggestedTag' + db.delete_table('askbot_suggestedtag') + + # Removing M2M table for field threads on 'SuggestedTag' + db.delete_table('askbot_suggestedtag_threads') + + # Removing M2M table for field users on 'SuggestedTag' + db.delete_table('askbot_suggestedtag_users') + + 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'}), + 'summary': ('django.db.models.fields.CharField', [], {'max_length': '180'}), + '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'}), + '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'}) + }, + '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.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.groupmembership': { + 'Meta': {'unique_together': "(('group', 'user'),)", 'object_name': 'GroupMembership'}, + 'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'user_memberships'", 'to': "orm['askbot.Tag']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'group_memberships'", 'to': "orm['auth.User']"}) + }, + 'askbot.groupprofile': { + 'Meta': {'object_name': 'GroupProfile'}, + 'group_tag': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'group_profile'", 'unique': 'True', 'to': "orm['askbot.Tag']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_open': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'logo_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True'}), + 'moderate_email': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + '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.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', 'to': "orm['askbot.Tag']"}), + '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'}), + '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']"}), + 'post_type': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'summary': ('django.db.models.fields.CharField', [], {'max_length': '180'}), + '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.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.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.suggestedtag': { + 'Meta': {'ordering': "['-used_count', 'name']", 'object_name': 'SuggestedTag'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'thread_ids': ('django.db.models.fields.TextField', [], {}), + 'threads': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['askbot.Thread']", 'symmetrical': 'False'}), + 'used_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'symmetrical': 'False'}) + }, + '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'}), + '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.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'}), + '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', 'to': "orm['askbot.Tag']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + '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']"}), + 'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + '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.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.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_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + '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'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}), + 'subscribed_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + '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': '30'}), + '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 527c45bc..046f19a1 100644 --- a/askbot/models/__init__.py +++ b/askbot/models/__init__.py @@ -29,7 +29,8 @@ from askbot.skins import utils as skin_utils from askbot.mail import messages from askbot.models.question import QuestionView, AnonymousQuestion from askbot.models.question import FavoriteQuestion -from askbot.models.tag import Tag, MarkedTag, get_group_names, get_groups +from askbot.models.tag import Tag, MarkedTag, SuggestedTag +from askbot.models.tag import get_group_names, get_groups from askbot.models.user import EmailFeedSetting, ActivityAuditStatus, Activity from askbot.models.user import GroupMembership, GroupProfile from askbot.models.post import Post, PostRevision, PostFlagReason, AnonymousAnswer @@ -343,43 +344,54 @@ def user_can_create_tags(self): else: return True -def user_try_creating_tags(self,tag_names): +def user_try_creating_tags(self, tag_names = None, thread = None): created_tags = list() if self.can_create_tags(): + suggested_tags = SuggestedTag.objects.filter(name__in = tag_names) + suggested_tags_dict = dict([(tag.name, tag) for tag in suggested_tags]) for name in tag_names: + #todo: keep better track of who creates the tag + suggested_tag = suggested_tags_dict.get(name, None) + if suggested_tag: + creator = suggested_tag.users.all()[0] + else: + creator = self + new_tag = Tag.objects.create( name = name, - created_by = self, - used_count = 1 + created_by = creator, + used_count = 1#wrong, but we are recounting downstream ) created_tags.append(new_tag) - #finally add tags to the relation and extend the modified list + #remove added tags from the suggested tag list + suggested_tags.delete() elif tag_names:#notify admins by email about new tags #maybe remove tags to report based on categories #todo: maybe move this to tags_updated signal handler if askbot_settings.TAG_SOURCE == 'category-tree': category_names = category_tree.get_leaf_names() + #remove category tree tags from creation list tag_names = tag_names - category_names + #here we put tags on the moderation queue if len(tag_names) > 0: - #todo: move all message sending codes to a separate module - body_text = messages.notify_admins_about_new_tags( - tags = tag_names, - user = self, - thread = self - ) - site_name = askbot_settings.APP_SHORT_NAME - subject_line = _('New tags added to %s') % site_name - mail.mail_moderators( - subject_line, - body_text, - headers = {'Reply-To': self.email} + suggested_tags = SuggestedTag.objects.filter(name__in = tag_names) + + previously_suggested_tag_names = set() + for tag in suggested_tags: + tag.used_count += 1 + tag.threads.add(thread) + tag.users.add(self) + tag.save() + previously_suggested_tag_names.add(tag.name) + + tag_names = set(tag_names) - previously_suggested_tag_names + + SuggestedTag.objects.create( + tag_names = tag_names, + user = self, + thread = thread ) - msg = _( - 'Tags %s are new and will be submitted for the ' - 'moderators approval' - ) % ', '.join(tag_names) - self.message_set.create(message = msg) return created_tags @@ -3412,6 +3424,7 @@ __all__ = [ 'Vote', 'PostFlagReason', 'MarkedTag', + 'SuggestedTag', 'BadgeData', 'Award', diff --git a/askbot/models/post.py b/askbot/models/post.py index e5a1867b..21723128 100644 --- a/askbot/models/post.py +++ b/askbot/models/post.py @@ -1508,7 +1508,9 @@ class Post(models.Model): # Update the Question tag associations if latest_revision.tagnames != tags: - self.thread.update_tags(tagnames = tags, user = edited_by, timestamp = edited_at) + self.thread.update_tags( + tagnames = tags, user = edited_by, timestamp = edited_at + ) self.thread.title = title self.thread.tagnames = tags diff --git a/askbot/models/question.py b/askbot/models/question.py index 446b4b1f..2a5c557d 100644 --- a/askbot/models/question.py +++ b/askbot/models/question.py @@ -129,7 +129,7 @@ class ThreadManager(BaseQuerySetManager): #this is kind of bad, but we save assign privacy groups to posts and thread question.parse_and_save(author = author, is_private = is_private) - question.add_revision( + revision = question.add_revision( author = author, is_anonymous = is_anonymous, text = text, @@ -794,7 +794,9 @@ class Thread(models.Model): return removed_tags - def update_tags(self, tagnames = None, user = None, timestamp = None): + def update_tags( + self, tagnames = None, user = None, timestamp = None + ): """ Updates Tag associations for a thread to match the given tagname string. @@ -811,8 +813,10 @@ class Thread(models.Model): """ previous_tags = list(self.tags.all()) + ordered_updated_tagnames = [t for t in tagnames.strip().split(' ')] + previous_tagnames = set([tag.name for tag in previous_tags]) - updated_tagnames = set(t for t in tagnames.strip().split(' ')) + updated_tagnames = set(ordered_updated_tagnames) removed_tagnames = previous_tagnames - updated_tagnames #remove tags from the question's tags many2many relation @@ -834,12 +838,26 @@ class Thread(models.Model): reused_tags.mark_undeleted() added_tags = list(reused_tags) - created_tags = user.try_creating_tags(new_tagnames) + #tag moderation is in the call below + created_tags = user.try_creating_tags(new_tagnames, thread = self) added_tags.extend(created_tags) #todo: not nice that assignment of added_tags is way above self.tags.add(*added_tags) modified_tags.extend(added_tags) + else: + added_tags = Tag.objects.none() + + #Save denormalized tag names on thread. Preserve order from user input. + added_tagnames = set([tag.name for tag in added_tags]) + final_tagnames = (previous_tagnames - removed_tagnames) | added_tagnames + ordered_final_tagnames = list() + for tagname in ordered_updated_tagnames: + if tagname in final_tagnames: + ordered_final_tagnames.append(tagname) + + self.tagnames = ' '.join(ordered_final_tagnames) + self.save()#need to save here? #################################################################### self.update_summary_html() # regenerate question/thread summary html diff --git a/askbot/models/tag.py b/askbot/models/tag.py index 95223bad..54c632e7 100644 --- a/askbot/models/tag.py +++ b/askbot/models/tag.py @@ -4,6 +4,7 @@ from django.contrib.auth.models import User from django.utils.translation import ugettext as _ from askbot.models.base import BaseQuerySetManager from askbot import const +from askbot.conf import settings as askbot_settings def delete_tags(tags): """deletes tags in the list""" @@ -236,3 +237,67 @@ def get_groups(): def get_group_names(): #todo: cache me return get_groups().values_list('name', flat = True) + +class SuggestedTagManager(models.Manager): + def create(self, tag_names = None, user = None, thread = None): + """creates ``SuggestedTag`` records and saves them + in the database""" + suggested_tags = list() + for tag_name in tag_names: + #create new record + suggested_tag = SuggestedTag(name = tag_name) + suggested_tag.save() + #add user and thread + suggested_tag.users.add(user) + suggested_tag.threads.add(thread) + #add to the list that is to be returned + suggested_tags.append(suggested_tag) + + #todo: stuff below will probably go after + #tag moderation actions are implemented + from askbot import mail + from askbot.mail import messages + body_text = messages.notify_admins_about_new_tags( + tags = tag_names, + user = user, + thread = thread + ) + site_name = askbot_settings.APP_SHORT_NAME + subject_line = _('New tags added to %s') % site_name + mail.mail_moderators( + subject_line, + body_text, + headers = {'Reply-To': user.email} + ) + + msg = _( + 'Tags %s are new and will be submitted for the ' + 'moderators approval' + ) % ', '.join(tag_names) + user.message_set.create(message = msg) + + return suggested_tags + +class SuggestedTag(models.Model): + """Suggested tag knows about who suggested it + and in what thread""" + name = models.CharField( + max_length=255, unique=True, help_text = 'Name for the proposed tag' + ) + used_count = models.PositiveIntegerField(default = 1) + #todo: instead these can be associated with revisions + #but the problem is that there would be too many joins + #to pull out threads + #if we used revisions, then we would not need to have + #a separate "user" field + threads = models.ManyToManyField('Thread') + users = models.ManyToManyField(User) + thread_ids = models.TextField( + help_text = 'comma-separated list of thread ids' + ) + + objects = SuggestedTagManager() + + class Meta: + app_label = 'askbot' + ordering = ['-used_count', 'name'] diff --git a/askbot/skins/default/media/style/style.less b/askbot/skins/default/media/style/style.less index 50acb24d..8d6b4afb 100644 --- a/askbot/skins/default/media/style/style.less +++ b/askbot/skins/default/media/style/style.less @@ -1219,10 +1219,22 @@ ul#related-tags li { color: #1A1A1A; } -.users-page h1, -.tags-page h1, -.groups-page h1 { - float: left; +.users-page, +.tags-page, +.groups-page, +.moderate-tags-page { + h1 { + float: left; + padding-top: 7px; + } +} + +.moderate-tags-page { + th, tr { + vertical-align: top; + text-align: left; + padding-right: 20px; + } } .main-page h1 { @@ -2299,7 +2311,6 @@ ul#related-tags li { /* tags page */ .tabBar-tags{ - width:270px; margin-bottom:15px; } diff --git a/askbot/skins/default/templates/moderate_tags.html b/askbot/skins/default/templates/moderate_tags.html new file mode 100644 index 00000000..59aa239a --- /dev/null +++ b/askbot/skins/default/templates/moderate_tags.html @@ -0,0 +1,39 @@ +{% extends "two_column_body.html" %} +{% import "macros.html" as macros %} + +{% block title %}{% spaceless %}{% trans %}Moderated Tags{% endtrans %}{% endspaceless %}{% endblock %} +{% block content %} + {% include "tags/header.html" %} + {% if tags %} + + + + + + + + + + + {% for tag in tags %} + + + + + + + {% endfor %} + +
{% trans %}Name{% endtrans %}{% trans %}Used count{% endtrans %}{% trans %}Suggested by{% endtrans %}{% trans %}In questions{% endtrans %}
{{ tag.name }}{{ tag.used_count }} + {% for user in tag.users.all() %} +

{{ user.get_profile_link() }}

+ {% endfor %} +
+ {% for thread in tag.threads.all() %} +

{{ thread.title|escape }}

+ {% endfor %} +
+ {% else %} + {% trans %}Nothing found{% endtrans %} + {% endif %} +{% endblock %} diff --git a/askbot/skins/default/templates/tags.html b/askbot/skins/default/templates/tags.html index 007388af..e9049e8e 100644 --- a/askbot/skins/default/templates/tags.html +++ b/askbot/skins/default/templates/tags.html @@ -4,30 +4,7 @@ {% block title %}{% spaceless %}{% trans %}Tags{% endtrans %}{% endspaceless %}{% endblock %} {% block content %} -
- {% if stag %} -

{% trans %}Tags, matching "{{ stag }}"{% endtrans %}

- {% else %} -

{% trans %}Tag list{% endtrans %}

- {% endif %} -
-
- {% trans %}Sort by »{% endtrans %} - {% trans %}by name{% endtrans %} - {% trans %}by popularity{% endtrans %} -
-
-
+{% include "tags/header.html" %} {% if tag_list_type == 'list' %} {% if not tags.object_list %} {% trans %}Nothing found{% endtrans %} diff --git a/askbot/skins/default/templates/tags/header.html b/askbot/skins/default/templates/tags/header.html new file mode 100644 index 00000000..4f614f30 --- /dev/null +++ b/askbot/skins/default/templates/tags/header.html @@ -0,0 +1,34 @@ +
+ {% if stag %} +

{% trans %}Tags, matching "{{ stag }}"{% endtrans %}

+ {% else %} +

{% trans %}Tags{% endtrans %}

+ {% endif %} +
+
+ {% trans %}Sort by »{% endtrans %} + {% trans %}by name{% endtrans %} + {% trans %}by popularity{% endtrans %} + {% if settings.ENABLE_TAG_MODERATION %} + {% if request.user.is_authenticated() and request.user.is_administrator_or_moderator() %} + {% trans %}moderated{% endtrans %} + {% endif %} + {% endif %} +
+
+
+
diff --git a/askbot/urls.py b/askbot/urls.py index 99b94860..7ea69579 100644 --- a/askbot/urls.py +++ b/askbot/urls.py @@ -159,6 +159,11 @@ urlpatterns = patterns('', views.readers.tags, name='tags' ), + url( + r'^%s$' % _('moderate-tags/'), + views.meta.moderate_tags, + name = 'moderate_tags' + ), #todo: collapse these three urls and use an extra json data var url(#ajax only r'^%s%s$' % ('mark-tag/', 'interesting/'), diff --git a/askbot/views/meta.py b/askbot/views/meta.py index b8411b41..f9cbe602 100644 --- a/askbot/views/meta.py +++ b/askbot/views/meta.py @@ -5,6 +5,7 @@ This module contains a collection of views displaying all sorts of secondary and """ from django.shortcuts import render_to_response, get_object_or_404 from django.core.urlresolvers import reverse +from django.core.paginator import Paginator, EmptyPage, InvalidPage from django.template import RequestContext, Template from django.http import HttpResponseRedirect, HttpResponse, Http404 from django.core.urlresolvers import reverse @@ -12,14 +13,16 @@ from django.utils.translation import ugettext as _ from django.views import static from django.views.decorators import csrf from django.db.models import Max, Count +from askbot import skins +from askbot.conf import settings as askbot_settings from askbot.forms import FeedbackForm -from askbot.utils.forms import get_next_url from askbot.mail import mail_moderators -from askbot.models import BadgeData, Award, User +from askbot.models import BadgeData, Award, User, SuggestedTag from askbot.models import badges as badge_data from askbot.skins.loaders import get_template, render_into_skin, render_text_into_skin -from askbot.conf import settings as askbot_settings -from askbot import skins +from askbot.utils.decorators import admins_only +from askbot.utils.forms import get_next_url +from askbot.utils import functions def generic_view(request, template = None, page_class = None): """this may be not necessary, since it is just a rewrite of render_into_skin""" @@ -153,3 +156,40 @@ def badge(request, id): 'page_class': 'meta', } return render_into_skin('badge.html', data, request) + +@admins_only +def moderate_tags(request): + """moderators and administrators can list tags that are + in the moderation queue, apply suggested tag to questions + or cancel the moderation reuest.""" + if askbot_settings.ENABLE_TAG_MODERATION == False: + raise Http404 + tags = SuggestedTag.objects.all() + #paginate moderated tags + paginator = Paginator(SuggestedTag.objects.all(), 20) + + page_no = request.GET.get('page', '1') + + try: + page = paginator.page(page_no) + except (EmptyPage, InvalidPage): + page = paginator.page(paginator.num_pages) + + paginator_context = functions.setup_paginator({ + 'is_paginated' : True, + 'pages': paginator.num_pages, + 'page': page_no, + 'has_previous': page.has_previous(), + 'has_next': page.has_next(), + 'previous': page.previous_page_number(), + 'next': page.next_page_number(), + 'base_url' : request.path + }) + + data = { + 'tags': page.object_list, + 'active_tab': 'tags', + 'page_class': 'moderate-tags-page', + 'paginator_context' : paginator_context, + } + return render_into_skin('moderate_tags.html', data, request) diff --git a/askbot/views/readers.py b/askbot/views/readers.py index 1bf37450..b37cacb2 100644 --- a/askbot/views/readers.py +++ b/askbot/views/readers.py @@ -244,23 +244,22 @@ def tags(request):#view showing a listing of available tags - plain list except ValueError: page = 1 - if request.method == "GET": - stag = request.GET.get("query", "").strip() - if stag != '': - objects_list = Paginator( - models.Tag.objects.filter( - deleted=False, - name__icontains=stag - ).exclude( - used_count=0 - ), - DEFAULT_PAGE_SIZE - ) + stag = request.GET.get("query", "").strip() + if stag != '': + objects_list = Paginator( + models.Tag.objects.filter( + deleted=False, + name__icontains=stag + ).exclude( + used_count=0 + ), + DEFAULT_PAGE_SIZE + ) + else: + if sortby == "name": + objects_list = Paginator(models.Tag.objects.all().filter(deleted=False).exclude(used_count=0).order_by("name"), DEFAULT_PAGE_SIZE) else: - if sortby == "name": - objects_list = Paginator(models.Tag.objects.all().filter(deleted=False).exclude(used_count=0).order_by("name"), DEFAULT_PAGE_SIZE) - else: - objects_list = Paginator(models.Tag.objects.all().filter(deleted=False).exclude(used_count=0).order_by("-used_count"), DEFAULT_PAGE_SIZE) + objects_list = Paginator(models.Tag.objects.all().filter(deleted=False).exclude(used_count=0).order_by("-used_count"), DEFAULT_PAGE_SIZE) try: tags = objects_list.page(page) -- cgit v1.2.3-1-g7c22