diff options
23 files changed, 751 insertions, 68 deletions
diff --git a/askbot/const/__init__.py b/askbot/const/__init__.py index 2bed1c31..2633055f 100644 --- a/askbot/const/__init__.py +++ b/askbot/const/__init__.py @@ -290,10 +290,11 @@ TYPE_RESPONSE = { } POST_STATUS = { - 'closed' : _('[closed]'), - 'deleted' : _('[deleted]'), - 'default_version' : _('initial version'), - 'retagged' : _('retagged'), + 'closed': _('[closed]'), + 'deleted': _('[deleted]'), + 'default_version': _('initial version'), + 'retagged': _('retagged'), + 'private': _('[private]') } #choices used in email and display filters diff --git a/askbot/forms.py b/askbot/forms.py index 5e4b5a06..5a8d75f1 100644 --- a/askbot/forms.py +++ b/askbot/forms.py @@ -626,7 +626,7 @@ class FeedbackForm(forms.Form): return self.cleaned_data -class FormWithHideableFields(object): +class FormWithHideableFields(forms.Form): """allows to swap a field widget to HiddenInput() and back""" def hide_field(self, name): @@ -647,7 +647,35 @@ class FormWithHideableFields(object): if name in self.__hidden_fields: self.fields[name] = self.__hidden_fields.pop(name) -class AskForm(forms.Form, FormWithHideableFields): +class PostPrivatelyForm(FormWithHideableFields): + """has a single field `post_privately` with + two related methods""" + + post_privately = forms.BooleanField( + label = _('keep private within your groups'), + required = False + ) + def __init__(self, *args, **kwargs): + user = kwargs.pop('user', None) + self._user = user + super(PostPrivatelyForm, self).__init__(*args, **kwargs) + if self.allows_post_privately() == False: + self.hide_field('post_privately') + + def allows_post_privately(self): + user = self._user + return ( + user and user.is_authenticated() and \ + user.can_make_group_private_posts() + ) + + def clean_post_privately(self): + if self.allows_post_privately() == False: + self.cleaned_data['post_privately'] = False + return self.cleaned_data['post_privately'] + + +class AskForm(PostPrivatelyForm): """the form used to askbot questions field ask_anonymously is shown to the user if the if ALLOW_ASK_ANONYMOUSLY live setting is True @@ -667,6 +695,7 @@ class AskForm(forms.Form, FormWithHideableFields): ), required = False, ) + openid = forms.CharField(required=False, max_length=255, widget=forms.TextInput(attrs={'size' : 40, 'class':'openid-input'})) user = forms.CharField(required=False, max_length=255, widget=forms.TextInput(attrs={'size' : 35})) email = forms.CharField(required=False, max_length=255, widget=forms.TextInput(attrs={'size' : 35})) @@ -684,6 +713,7 @@ class AskForm(forms.Form, FormWithHideableFields): self.cleaned_data['ask_anonymously'] = False return self.cleaned_data['ask_anonymously'] + ASK_BY_EMAIL_SUBJECT_HELP = _( 'Subject line is expected in the format: ' '[tag1, tag2, tag3,...] question title' @@ -766,7 +796,7 @@ class AskByEmailForm(forms.Form): raise forms.ValidationError(ASK_BY_EMAIL_SUBJECT_HELP) return self.cleaned_data['subject'] -class AnswerForm(forms.Form): +class AnswerForm(PostPrivatelyForm): text = AnswerEditorField() wiki = WikiField() openid = forms.CharField(required=False, max_length=255, widget=forms.TextInput(attrs={'size' : 40, 'class':'openid-input'})) @@ -822,7 +852,7 @@ class RevisionForm(forms.Form): for r in revisions] self.fields['revision'].initial = latest_revision.revision -class EditQuestionForm(forms.Form, FormWithHideableFields): +class EditQuestionForm(PostPrivatelyForm): title = TitleField() text = QuestionEditorField() tags = TagNamesField() @@ -842,7 +872,7 @@ class EditQuestionForm(forms.Form, FormWithHideableFields): def __init__(self, *args, **kwargs): """populate EditQuestionForm with initial data""" self.question = kwargs.pop('question') - self.user = kwargs.pop('user') + self.user = kwargs['user']#preserve for superclass revision = kwargs.pop('revision') super(EditQuestionForm, self).__init__(*args, **kwargs) self.fields['title'].initial = revision.title @@ -853,6 +883,15 @@ class EditQuestionForm(forms.Form, FormWithHideableFields): if not self.can_stay_anonymous(): self.hide_field('reveal_identity') + def has_changed(self): + if super(EditQuestionForm, self).has_changed(): + return True + if askbot_settings.GROUPS_ENABLED: + return self.question.is_private() \ + != self.cleaned_data['post_privately'] + else: + return False + def can_stay_anonymous(self): """determines if the user cat keep editing the question anonymously""" @@ -930,16 +969,28 @@ class EditQuestionForm(forms.Form, FormWithHideableFields): self.cleaned_data['stay_anonymous'] = stay_anonymous return self.cleaned_data -class EditAnswerForm(forms.Form): +class EditAnswerForm(PostPrivatelyForm): text = AnswerEditorField() summary = SummaryField() wiki = WikiField() def __init__(self, answer, revision, *args, **kwargs): + self.answer = answer super(EditAnswerForm, self).__init__(*args, **kwargs) self.fields['text'].initial = revision.text self.fields['wiki'].initial = answer.wiki + def has_changed(self): + #todo: this function is almost copy/paste of EditQuestionForm.has_changed() + if super(EditAnswerForm, self).has_changed(): + return True + if askbot_settings.GROUPS_ENABLED: + return self.answer.is_private() \ + != self.cleaned_data['post_privately'] + else: + return False + + class EditTagWikiForm(forms.Form): text = forms.CharField(required = False) tag_id = forms.IntegerField() diff --git a/askbot/management/commands/send_email_alerts.py b/askbot/management/commands/send_email_alerts.py index e890452d..5d58276d 100644 --- a/askbot/management/commands/send_email_alerts.py +++ b/askbot/management/commands/send_email_alerts.py @@ -84,7 +84,7 @@ class Command(NoArgsCommand): finally: connection.close() - def get_updated_questions_for_user(self,user): + def get_updated_questions_for_user(self, user): """ retreive relevant question updates for the user according to their subscriptions and recorded question @@ -353,7 +353,7 @@ class Command(NoArgsCommand): else: meta_data['new_q'] = False - new_ans = Post.objects.get_answers().filter( + new_ans = Post.objects.get_answers(user).filter( thread=q.thread, added_at__gt=emailed_at, deleted=False, diff --git a/askbot/migrations/0126_add_groups_field__to__thread_and_post.py b/askbot/migrations/0126_add_groups_field__to__thread_and_post.py new file mode 100644 index 00000000..ab8bf919 --- /dev/null +++ b/askbot/migrations/0126_add_groups_field__to__thread_and_post.py @@ -0,0 +1,335 @@ +# -*- 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 M2M table for field groups on 'Thread' + db.create_table('askbot_thread_groups', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('thread', models.ForeignKey(orm['askbot.thread'], null=False)), + ('tag', models.ForeignKey(orm['askbot.tag'], null=False)) + )) + db.create_unique('askbot_thread_groups', ['thread_id', 'tag_id']) + # Adding M2M table for field groups on 'Post' + db.create_table('askbot_post_groups', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('post', models.ForeignKey(orm['askbot.post'], null=False)), + ('tag', models.ForeignKey(orm['askbot.tag'], null=False)) + )) + db.create_unique('askbot_post_groups', ['post_id', 'tag_id']) + + def backwards(self, orm): + + # Removing M2M tables for field groups on 'Thread' and 'Post' + db.delete_table('askbot_thread_groups') + db.delete_table('askbot_post_groups') + + 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']"}), + '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'}), + 'is_private': ('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', [], {}), + 'revision_type': ('django.db.models.fields.SmallIntegerField', [], {}), + '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.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'] diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py index 4acbbdc7..6c6bb270 100644 --- a/askbot/models/__init__.py +++ b/askbot/models/__init__.py @@ -27,7 +27,7 @@ from askbot.models.question import Thread from askbot.skins import utils as skin_utils from askbot.models.question import QuestionView, AnonymousQuestion from askbot.models.question import FavoriteQuestion -from askbot.models.tag import Tag, MarkedTag +from askbot.models.tag import Tag, MarkedTag, 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 @@ -188,6 +188,9 @@ def user_get_avatar_url(self, size): else: return self.get_default_avatar_url(size) +def user_get_user_groups(self): + """returns query set of groups to which user belongs""" + def user_update_avatar_type(self): """counts number of custom avatars and if zero, sets avatar_type to False, @@ -1405,6 +1408,7 @@ def user_post_question( tags = None, wiki = False, is_anonymous = False, + is_private = False, timestamp = None, by_email = False, email_address = None @@ -1434,6 +1438,7 @@ def user_post_question( added_at = timestamp, wiki = wiki, is_anonymous = is_anonymous, + is_private = is_private, by_email = by_email, email_address = email_address ) @@ -1471,7 +1476,8 @@ def user_edit_post(self, body_text = None, revision_comment = None, timestamp = None, - by_email = False + by_email = False, + is_private = False ): """a simple method that edits post body todo: unify it in the style of just a generic post @@ -1498,7 +1504,8 @@ def user_edit_post(self, body_text = body_text, timestamp = timestamp, revision_comment = revision_comment, - by_email = by_email + by_email = by_email, + is_private = is_private ) elif post.post_type == 'tag_wiki': post.apply_edit( @@ -1523,6 +1530,7 @@ def user_edit_question( tags = None, wiki = False, edit_anonymously = False, + is_private = False, timestamp = None, force = False,#if True - bypass the assert by_email = False @@ -1540,6 +1548,7 @@ def user_edit_question( tags = tags, wiki = wiki, edit_anonymously = edit_anonymously, + is_private = is_private, by_email = by_email ) @@ -1559,6 +1568,7 @@ def user_edit_answer( body_text = None, revision_comment = None, wiki = False, + is_private = False, timestamp = None, force = False,#if True - bypass the assert by_email = False @@ -1629,6 +1639,7 @@ def user_post_answer( body_text = None, follow = False, wiki = False, + is_private = False, timestamp = None, by_email = False ): @@ -1696,6 +1707,13 @@ def user_post_answer( wiki = wiki, by_email = by_email ) + + if self.can_make_group_private_posts(): + if is_private: + answer_post.make_private(self) + else: + answer_post.make_public(self) + answer_post.thread.invalidate_cached_data() award_badges_signal.send(None, event = 'post_answer', @@ -2067,6 +2085,21 @@ def get_profile_link(self): return mark_safe(profile_link) +def user_get_groups(self): + """returns a query set of groups to which user belongs""" + #todo: maybe cache this query + return Tag.group_tags.get_for_user(self) + +def user_get_foreign_groups(self): + """returns a query set of groups to which user does not belong""" + #todo: maybe cache this query + user_group_ids = self.get_groups().values_list('id', flat = True) + return get_groups().exclude(id__in = user_group_ids) + +def user_can_make_group_private_posts(self): + """simplest implementation: user belongs to at least one group""" + return self.get_groups().count() > 0 + def user_get_groups_membership_info(self, groups): """returts a defaultdict with values that are dictionaries with the following keys and values: @@ -2483,6 +2516,8 @@ User.add_to_class('get_absolute_url', user_get_absolute_url) User.add_to_class('get_avatar_url', user_get_avatar_url) User.add_to_class('get_default_avatar_url', user_get_default_avatar_url) User.add_to_class('get_gravatar_url', user_get_gravatar_url) +User.add_to_class('get_groups', user_get_groups) +User.add_to_class('get_foreign_groups', user_get_foreign_groups) User.add_to_class('strip_email_signature', user_strip_email_signature) User.add_to_class('get_groups_membership_info', user_get_groups_membership_info) User.add_to_class('get_anonymous_name', user_get_anonymous_name) @@ -2529,6 +2564,7 @@ User.add_to_class('can_create_tags', user_can_create_tags) User.add_to_class('can_have_strong_url', user_can_have_strong_url) User.add_to_class('can_post_by_email', user_can_post_by_email) User.add_to_class('can_post_comment', user_can_post_comment) +User.add_to_class('can_make_group_private_posts', user_can_make_group_private_posts) User.add_to_class('is_administrator', user_is_administrator) User.add_to_class('is_administrator_or_moderator', user_is_administrator_or_moderator) User.add_to_class('set_admin_status', user_set_admin_status) @@ -3310,5 +3346,7 @@ __all__ = [ 'ReplyAddress', 'get_model', - 'get_admins_and_moderators' + 'get_admins_and_moderators', + 'get_group_names', + 'get_grous' ] diff --git a/askbot/models/post.py b/askbot/models/post.py index 673b4c6f..c895084d 100644 --- a/askbot/models/post.py +++ b/askbot/models/post.py @@ -24,7 +24,8 @@ import askbot from askbot.utils.slug import slugify from askbot import const from askbot.models.user import EmailFeedSetting -from askbot.models.tag import Tag, MarkedTag, tags_match_some_wildcard +from askbot.models.tag import Tag, MarkedTag +from askbot.models.tag import get_groups, tags_match_some_wildcard from askbot.conf import settings as askbot_settings from askbot import exceptions from askbot.utils import markup @@ -142,8 +143,21 @@ class PostManager(BaseQuerySetManager): def get_questions(self): return self.filter(post_type='question') - def get_answers(self): - return self.filter(post_type='answer') + def get_answers(self, user = None): + """returns query set of answer posts, + optionally filtered to exclude posts of groups + to which user does not belong""" + answers = self.filter(post_type='answer') + + if askbot_settings.GROUPS_ENABLED: + if user is None or user.is_anonymous(): + exclude_groups = get_groups() + else: + exclude_groups = user.get_foreign_groups() + answers = answers.exclude(groups__in = exclude_groups) + + return answers + def get_comments(self): return self.filter(post_type='comment') @@ -288,6 +302,7 @@ class Post(models.Model): parent = models.ForeignKey('Post', blank=True, null=True, related_name='comments') # Answer or Question for Comment thread = models.ForeignKey('Thread', blank=True, null=True, default = None, related_name='posts') + groups = models.ManyToManyField('Tag', related_name = 'group_posts')#used for group-private posts author = models.ForeignKey(User, related_name='posts') added_at = models.DateTimeField(default=datetime.datetime.now) @@ -511,6 +526,24 @@ class Post(models.Model): def is_reject_reason(self): return self.post_type == 'reject_reason' + def make_private(self, user): + """makes post private within user's groups""" + groups = user.get_groups() + self.groups.add(*groups) + if self.is_question(): + self.thread.groups.add(*groups) + + def make_public(self, user): + """removes the privacy mark from users groups""" + groups = user.get_groups() + self.groups.remove(*groups) + if self.is_question(): + self.thread.groups.remove(*groups) + + def is_private(self): + """true, if post is private within any groups""" + return askbot_settings.GROUPS_ENABLED and self.groups.count() > 0 + def needs_moderation(self): return self.approved == False @@ -900,7 +933,11 @@ class Post(models.Model): #4) question asked by me (todo: not "edited_by_me" ???) question_author = origin_post.author - if EmailFeedSetting.objects.filter(subscriber = question_author, frequency = 'i', feed_type = 'q_ask').exists(): + if EmailFeedSetting.objects.filter( + subscriber = question_author, + frequency = 'i', + feed_type = 'q_ask' + ).exists(): subscriber_set.add(question_author) #4) questions answered by me -make sure is that people @@ -1262,8 +1299,32 @@ class Post(models.Model): ) raise exceptions.AnswerHidden(message) + def assert_is_visible_to_user_groups(self, user): + """raises permission denied of the post + is hidden due to group memberships""" + assert(self.is_comment() == False) + post_groups = self.groups.all() + if post_groups.count() == 0: + return + + if self.is_question():#todo maybe merge the "hidden" exceptions + exception = exceptions.QuestionHidden + elif self.is_answer(): + exception = exceptions.AnswerHidden + else: + raise NotImplementedError + + message = _('This post is temporarily not available') + if user.is_anonymous(): + raise exception(message) + else: + user_groups_ids = user.get_groups().values_list('id', flat = True) + if post_groups.filter(id__in = user_groups_ids).count() == 0: + raise exception(message) def assert_is_visible_to(self, user): + if self.is_comment() == False and askbot_settings.GROUPS_ENABLED: + self.assert_is_visible_to_user_groups(user) if self.is_question(): return self._question__assert_is_visible_to(user) elif self.is_answer(): @@ -1371,7 +1432,7 @@ class Post(models.Model): def _question__apply_edit(self, edited_at=None, edited_by=None, title=None,\ text=None, comment=None, tags=None, wiki=False,\ - edit_anonymously = False, + edit_anonymously = False, is_private = False, by_email = False ): @@ -1395,6 +1456,12 @@ class Post(models.Model): self.thread.tagnames = tags self.thread.save() + if self.is_private() != is_private: + if is_private: + self.make_private(self.author) + else: + self.make_public(self.author) + self.__apply_edit( edited_at = edited_at, edited_by = edited_by, diff --git a/askbot/models/question.py b/askbot/models/question.py index 13cbf65d..2e8faa06 100644 --- a/askbot/models/question.py +++ b/askbot/models/question.py @@ -15,8 +15,9 @@ 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.base import AnonymousContent +from askbot.models.tag import Tag, get_groups +from askbot.models.base import AnonymousContent, BaseQuerySetManager +from askbot.models.tag import Tag, get_groups from askbot.models.post import Post, PostRevision from askbot.models import signals from askbot import const @@ -26,8 +27,21 @@ from askbot.utils.slug import slugify from askbot.skins.loaders import get_template #jinja2 template loading enviroment from askbot.search.state_manager import DummySearchState +class ThreadQuerySet(models.query.QuerySet): + def exclude_group_private(self, user): + """filters out threads not belonging to the user groups""" + if user.is_authenticated(): + groups = user.get_foreign_groups() + else: + groups = get_groups() + #todo: maybe use is_private field + return self.exclude(groups__in = groups) + +class ThreadManager(BaseQuerySetManager): + + def get_query_set(self): + return ThreadQuerySet(self.model) -class ThreadManager(models.Manager): def get_tag_summary_from_threads(self, threads): """returns a humanized string containing up to five most frequently used @@ -73,6 +87,7 @@ class ThreadManager(models.Manager): text, tagnames = None, is_anonymous = False, + is_private = False, by_email = False, email_address = None ): @@ -122,6 +137,9 @@ class ThreadManager(models.Manager): email_address = email_address ) + if is_private:#add groups to thread and question + thread.make_private(author) + # INFO: Question has to be saved before update_tags() is called thread.update_tags(tagnames = tagnames, user = author, timestamp = added_at) @@ -175,15 +193,39 @@ class ThreadManager(models.Manager): meta_data = {} + #run text search while excluding any modifier in the search string + #like #tag [title: something] @user if search_state.stripped_query: qs = self.get_for_query(search_query=search_state.stripped_query, qs=qs) + + #we run other things after full text search, because + #FTS may break the chain of the query set calls, + #since it might go into an external asset, like Solr + + #if groups feature is enabled, filter out threads + #that are private in groups to which current user does not belong + if askbot_settings.GROUPS_ENABLED: + #get group names + qs = self.exclude_group_private(user = request_user) + + #search in titles, if necessary if search_state.query_title: qs = qs.filter(title__icontains = search_state.query_title) + + #search user names if @user is added to search string + #or if user name exists in the search state if search_state.query_users: query_users = User.objects.filter(username__in=search_state.query_users) if query_users: - qs = qs.filter(posts__post_type='question', posts__author__in=query_users) # TODO: unify with search_state.author ? - + qs = qs.filter( + posts__post_type='question', + posts__author__in=query_users + ) # TODO: unify with search_state.author ? + + #unified tags - is list of tags taken from the tag selection + #plus any tags added to the query string with #tag or [tag:something] + #syntax. + #run tag search in addition to these unified tags tags = search_state.unified_tags() if len(tags) > 0: @@ -214,6 +256,8 @@ class ThreadManager(models.Manager): if search_state.scope == 'unanswered': qs = qs.filter(closed = False) # Do not show closed questions in unanswered section if askbot_settings.UNANSWERED_QUESTION_MEANING == 'NO_ANSWERS': + # todo: this will introduce a problem if there are private answers + # which are counted here qs = qs.filter(answer_count=0) # TODO: expand for different meanings of this elif askbot_settings.UNANSWERED_QUESTION_MEANING == 'NO_ACCEPTED_ANSWERS': qs = qs.filter(accepted_answer__isnull=True) @@ -319,8 +363,13 @@ class ThreadManager(models.Manager): # Precache data only for non-cached threads - only those will be rendered #threads = [thread for thread in threads if not thread.summary_html_cached()] - page_questions = Post.objects.filter(post_type='question', thread__in=[obj.id for obj in threads])\ - .only('id', 'thread', 'score', 'is_anonymous', 'summary', 'post_type', 'deleted') # pick only the used fields + thread_ids = [obj.id for obj in threads] + page_questions = Post.objects.filter( + post_type='question', thread__id__in = thread_ids + ).only(# pick only the used fields + 'id', 'thread', 'score', 'is_anonymous', + 'summary', 'post_type', 'deleted' + ) page_question_map = {} for pq in page_questions: page_question_map[pq.thread_id] = pq @@ -374,6 +423,7 @@ class Thread(models.Model): title = models.CharField(max_length=300) tags = models.ManyToManyField('Tag', related_name='threads') + groups = models.ManyToManyField('Tag', related_name='group_threads') # Denormalised data, transplanted from Question tagnames = models.CharField(max_length=125) @@ -425,6 +475,15 @@ class Thread(models.Model): #question_id = self._question_post().id #return reverse('question', args = [question_id]) + slugify(self.title) + def get_answer_count(self, user = None): + """returns answer count depending on who the user is. + When user groups are enabled and some answers are hidden, + the answer count to show must be reflected accordingly""" + if askbot_settings.GROUPS_ENABLED == False or user is None: + return self.answer_count + else: + return self.get_answers(user).count() + def update_favorite_count(self): self.favourite_count = FavoriteQuestion.objects.filter(thread=self).count() self.save() @@ -474,10 +533,13 @@ class Thread(models.Model): def get_title(self, question=None): if not question: question = self._question_post() # allow for optimization if the caller has already fetched the question post for this thread - if self.closed: + if self.is_private(): + attr = const.POST_STATUS['private'] + elif self.closed: attr = const.POST_STATUS['closed'] elif question.deleted: attr = const.POST_STATUS['deleted'] + else: attr = None if attr is not None: @@ -514,12 +576,12 @@ class Thread(models.Model): return self.posts.get_answers().filter(deleted=False) else: if user.is_administrator() or user.is_moderator(): - return self.posts.get_answers() + return self.posts.get_answers(user = user) else: - return self.posts.get_answers().filter( - models.Q(deleted = False) | models.Q(author = user) \ - | models.Q(deleted_by = user) - ) + return self.posts.get_answers(user = user).filter( + models.Q(deleted = False) | models.Q(author = user) \ + | models.Q(deleted_by = user) + ) def invalidate_cached_thread_content_fragment(self): cache.cache.delete(self.SUMMARY_CACHE_KEY_TPL % self.id) @@ -540,9 +602,12 @@ class Thread(models.Model): #self.invalidate_cached_thread_content_fragment() self.update_summary_html() - def get_cached_post_data(self, sort_method = 'votes'): + def get_cached_post_data(self, user = None, sort_method = 'votes'): """returns cached post data, as calculated by the method get_post_data()""" + if askbot_settings.GROUPS_ENABLED: + #temporary plug: bypass cache where groups are enabled + return self.get_post_data(sort_method = sort_method, user = user) key = self.get_post_data_cache_key(sort_method) post_data = cache.cache.get(key) if not post_data: @@ -550,14 +615,22 @@ class Thread(models.Model): cache.cache.set(key, post_data, const.LONG_TIME) return post_data - def get_post_data(self, sort_method = 'votes'): + def get_post_data(self, sort_method = 'votes', user = None): """returns question, answers as list and a list of post ids for the given thread the returned posts are pre-stuffed with the comments all (both posts and the comments sorted in the correct order) """ - thread_posts = self.posts.all().order_by( + thread_posts = self.posts.all() + if askbot_settings.GROUPS_ENABLED: + if user.is_anonymous(): + exclude_groups = get_groups() + else: + exclude_groups = user.get_foreign_groups() + thread_posts = thread_posts.exclude(groups__in = exclude_groups) + + thread_posts = thread_posts.order_by( { 'latest':'-added_at', 'oldest':'added_at', @@ -700,6 +773,14 @@ class Thread(models.Model): return self.followed_by.filter(id = user.id).count() > 0 return False + def make_private(self, user): + groups = list(user.get_groups()) + self.groups.add(*groups) + self._question_post().groups.add(*groups) + + def is_private(self): + return askbot_settings.GROUPS_ENABLED and self.groups.count() > 0 + def update_tags(self, tagnames = None, user = None, timestamp = None): """ Updates Tag associations for a thread to match the given @@ -865,11 +946,13 @@ class Thread(models.Model): return last_updated_at, last_updated_by - def get_summary_html(self, search_state): - html = self.get_cached_summary_html() + def get_summary_html(self, search_state, visitor = None): + html = self.get_cached_summary_html(visitor) if not html: - html = self.update_summary_html() + html = self.update_summary_html(visitor) + # todo: this work may be pushed onto javascript we post-process tag names + # in the snippet so that tag urls match the search state # use `<<<` and `>>>` because they cannot be confused with user input # - if user accidentialy types <<<tag-name>>> into question title or body, # then in html it'll become escaped like this: <<<tag-name>>> @@ -889,14 +972,21 @@ class Thread(models.Model): return html - def get_cached_summary_html(self): + def get_cached_summary_html(self, visitor = None): + #todo: remove this plug by adding cached foreign user group + #parameter to the key. Now with groups on caching is turned off + #parameter visitor is there to get summary out by the user groups + if askbot_settings.GROUPS_ENABLED: + return None return cache.cache.get(self.SUMMARY_CACHE_KEY_TPL % self.id) - def update_summary_html(self): + def update_summary_html(self, visitor = None): context = { 'thread': self, - 'question': self._question_post(refresh=True), # fetch new question post to make sure we're up-to-date + #fetch new question post to make sure we're up-to-date + 'question': self._question_post(refresh=True), 'search_state': DummySearchState(), + 'visitor': visitor } html = get_template('widgets/question_summary.html').render(context) # INFO: Timeout is set to 30 days: diff --git a/askbot/models/tag.py b/askbot/models/tag.py index 0c3af52f..0546ab50 100644 --- a/askbot/models/tag.py +++ b/askbot/models/tag.py @@ -185,3 +185,10 @@ class MarkedTag(models.Model): class Meta: app_label = 'askbot' + +def get_groups(): + return Tag.group_tags.get_all() + +def get_group_names(): + #todo: cache me + return get_groups().values_list('name', flat = True) diff --git a/askbot/skins/default/templates/answer_edit.html b/askbot/skins/default/templates/answer_edit.html index bbc00420..f19ae016 100644 --- a/askbot/skins/default/templates/answer_edit.html +++ b/askbot/skins/default/templates/answer_edit.html @@ -20,6 +20,9 @@ {% if settings.WIKI_ON and answer.wiki == False %} {{ macros.checkbox_in_div(form.wiki) }} {% endif %} + {% if request.user.is_authenticated() and request.user.can_make_group_private_posts() %} + {{ macros.checkbox_in_div(form.post_privately) }} + {% endif %} <div class="after-editor"> <input type="submit" value="{% trans %}Save edit{% endtrans %}" class="submit" /> <input type="button" value="{% trans %}Cancel{% endtrans %}" class="submit" onclick="history.back(-1);" /> diff --git a/askbot/skins/default/templates/main_page/questions_loop.html b/askbot/skins/default/templates/main_page/questions_loop.html index 6a5e5e3d..ad7bf683 100644 --- a/askbot/skins/default/templates/main_page/questions_loop.html +++ b/askbot/skins/default/templates/main_page/questions_loop.html @@ -1,12 +1,10 @@ {% import "macros.html" as macros %} -{# cache 0 "questions" questions search_tags scope sort query context.page language_code #} -{% for thread in threads.object_list %} - {# {{macros.question_summary(thread, thread._question_post(), search_state=search_state)}} #} - {{ thread.get_summary_html(search_state=search_state) }} -{% endfor %} {% if threads.object_list|length == 0 %} {% include "main_page/nothing_found.html" %} {% else %} + {% for thread in threads.object_list %} + {{ thread.get_summary_html(search_state=search_state, visitor = request.user) }} + {% endfor %} <div class="evenMore"> {% trans %}Did not find what you were looking for?{% endtrans %} <a href="{% url ask %}">{% trans %}Please, post your question!{% endtrans %}</a> diff --git a/askbot/skins/default/templates/question.html b/askbot/skins/default/templates/question.html index 5403a644..511c5086 100644 --- a/askbot/skins/default/templates/question.html +++ b/askbot/skins/default/templates/question.html @@ -56,6 +56,9 @@ if (data['userIsAdminOrMod']){ return;//all functions on } + if (data['user_posts'] === undefined) { + return; + } if (post_id in data['user_posts']){ //todo: remove edit button from older comments return;//same here diff --git a/askbot/skins/default/templates/question/new_answer_form.html b/askbot/skins/default/templates/question/new_answer_form.html index 68af8afb..347973bf 100644 --- a/askbot/skins/default/templates/question/new_answer_form.html +++ b/askbot/skins/default/templates/question/new_answer_form.html @@ -47,6 +47,9 @@ {% if settings.WIKI_ON %} {{ macros.checkbox_in_div(answer.wiki) }} {% endif %} + {% if request.user.is_authenticated() and request.user.can_make_group_private_posts() %} + {{ macros.checkbox_in_div(answer.post_privately) }} + {% endif %} {% endif %} {% endif %} </form> diff --git a/askbot/skins/default/templates/question_edit.html b/askbot/skins/default/templates/question_edit.html index 2576a1f1..a4ffa424 100644 --- a/askbot/skins/default/templates/question_edit.html +++ b/askbot/skins/default/templates/question_edit.html @@ -34,6 +34,9 @@ {% if form.can_stay_anonymous() %} {{ macros.checkbox_in_div(form.reveal_identity) }} {% endif %} + {% if request.user.is_authenticated() and request.user.can_make_group_private_posts() %} + {{ macros.checkbox_in_div(form.post_privately) }} + {% endif %} </div> <input type="submit" value="{% trans %}Save edit{% endtrans %}" class="submit" /> <input type="button" value="{% trans %}Cancel{% endtrans %}" class="submit" onclick="history.back(-1);" /> diff --git a/askbot/skins/default/templates/widgets/ask_form.html b/askbot/skins/default/templates/widgets/ask_form.html index 94b5e309..ae71b230 100644 --- a/askbot/skins/default/templates/widgets/ask_form.html +++ b/askbot/skins/default/templates/widgets/ask_form.html @@ -37,6 +37,9 @@ {% if settings.ALLOW_ASK_ANONYMOUSLY %} {{ macros.checkbox_in_div(form.ask_anonymously) }} {% endif %} + {% if request.user.is_authenticated() and request.user.can_make_group_private_posts() %} + {{ macros.checkbox_in_div(form.post_privately) }} + {% endif %} </div> {% if not request.user.is_authenticated() %} <input type="submit" name="post_anon" value="{% trans %}Login/Signup to Post{% endtrans %}" class="submit" /> diff --git a/askbot/skins/default/templates/widgets/question_summary.html b/askbot/skins/default/templates/widgets/question_summary.html index c6e7bc5d..22f52a42 100644 --- a/askbot/skins/default/templates/widgets/question_summary.html +++ b/askbot/skins/default/templates/widgets/question_summary.html @@ -12,8 +12,9 @@ {% trans cnt=thread.view_count %}view{% pluralize %}views{% endtrans %} </div> </div> + {% set answer_count = thread.get_answer_count(visitor) %} <div class="answers - {% if thread.answer_count == 0 -%} + {% if answer_count == 0 -%} no-answers {% else -%} {%- if thread.accepted_answer_id -%} {# INFO: Use _id to not fetch the whole answer post #} @@ -24,9 +25,9 @@ {%- endif -%}"> <span class="item-count" - >{{thread.answer_count|humanize_counter}}{% if thread.accepted_answer_id %}{% endif %}</span> + >{{ answer_count|humanize_counter }}{% if thread.accepted_answer_id %}{% endif %}</span> <div> - {% trans cnt=thread.answer_count %}answer{% pluralize %}answers{% endtrans %} + {% trans cnt = answer_count %}answer{% pluralize %}answers{% endtrans %} </div> </div> <div class="votes diff --git a/askbot/tests/__init__.py b/askbot/tests/__init__.py index 198bf9e8..fbf32726 100644 --- a/askbot/tests/__init__.py +++ b/askbot/tests/__init__.py @@ -14,5 +14,6 @@ from askbot.tests.templatefilter_tests import * from askbot.tests.markup_test import * from askbot.tests.misc_tests import * from askbot.tests.post_model_tests import * +from askbot.tests.thread_model_tests import * from askbot.tests.reply_by_email_tests import * from askbot.tests.category_tree_tests import CategoryTreeTests diff --git a/askbot/tests/post_model_tests.py b/askbot/tests/post_model_tests.py index 9a4d47c8..ac09c667 100644 --- a/askbot/tests/post_model_tests.py +++ b/askbot/tests/post_model_tests.py @@ -418,7 +418,7 @@ class ThreadRenderLowLevelCachingTests(AskbotTestCase): ### cache.cache.delete(key) - thread.update_summary_html = lambda: "Monkey-patched <<<tag2>>>" + thread.update_summary_html = lambda dummy: "Monkey-patched <<<tag2>>>" self.assertFalse(thread.summary_html_cached()) self.assertIsNone(thread.get_cached_summary_html()) diff --git a/askbot/tests/thread_model_tests.py b/askbot/tests/thread_model_tests.py new file mode 100644 index 00000000..634a4b0a --- /dev/null +++ b/askbot/tests/thread_model_tests.py @@ -0,0 +1,40 @@ +from askbot.tests.utils import AskbotTestCase +from askbot.conf import settings as askbot_settings +from askbot import models + +class ThreadModelTestsWithGroupsEnabled(AskbotTestCase): + + def setUp(self): + self.groups_enabled_backup = askbot_settings.GROUPS_ENABLED + askbot_settings.update('GROUPS_ENABLED', True) + self.admin = self.create_user('admin', status = 'd') + self.user = self.create_user('user') + self.group = models.Tag.group_tags.get_or_create( + group_name = 'jockeys', user = self.admin + ) + self.admin.edit_group_membership( + group = self.group, + user = self.admin, + action = 'add' + ) + + def tearDown(self): + askbot_settings.update('GROUPS_ENABLED', self.groups_enabled_backup) + + def test_private_answer(self): + # post question, answer, add answer to the group + self.question = self.post_question(self.user) + + self.answer = self.post_answer( + user = self.admin, + question = self.question, + is_private = True + ) + + thread = self.question.thread + + #test answer counts + self.assertEqual(thread.get_answer_count(self.user), 0) + self.assertEqual(thread.get_answer_count(self.admin), 1) + + #test mail outbox diff --git a/askbot/tests/utils.py b/askbot/tests/utils.py index 4bc69ac4..7b4134d1 100644 --- a/askbot/tests/utils.py +++ b/askbot/tests/utils.py @@ -118,6 +118,7 @@ class AskbotTestCase(TestCase): by_email = False, wiki = False, is_anonymous = False, + is_private = False, follow = False, timestamp = None, ): @@ -139,6 +140,7 @@ class AskbotTestCase(TestCase): by_email = by_email, wiki = wiki, is_anonymous = is_anonymous, + is_private = is_private, timestamp = timestamp ) @@ -160,6 +162,7 @@ class AskbotTestCase(TestCase): by_email = False, follow = False, wiki = False, + is_private = False, timestamp = None ): @@ -171,6 +174,7 @@ class AskbotTestCase(TestCase): by_email = by_email, follow = follow, wiki = wiki, + is_private = is_private, timestamp = timestamp ) diff --git a/askbot/views/commands.py b/askbot/views/commands.py index a1c13708..979309ba 100644 --- a/askbot/views/commands.py +++ b/askbot/views/commands.py @@ -15,6 +15,7 @@ from django.forms import ValidationError, IntegerField, CharField from django.shortcuts import get_object_or_404 from django.views.decorators import csrf from django.utils import simplejson +from django.utils.html import escape from django.utils.translation import ugettext as _ from django.utils.translation import string_concat from askbot import models @@ -496,7 +497,7 @@ def get_tag_list(request): ).values_list( 'name', flat = True ) - output = '\n'.join(tag_names) + output = '\n'.join(map(escape, tag_names)) return HttpResponse(output, mimetype = 'text/plain') @decorators.get_only @@ -670,7 +671,7 @@ def api_get_questions(request): thread_list = [{ 'url': thread.get_absolute_url(), 'title': thread.title, - 'answer_count': thread.answer_count + 'answer_count': thread.get_answer_count(request.user) } for thread in threads] json_data = simplejson.dumps(thread_list) return HttpResponse(json_data, mimetype = "application/json") @@ -767,7 +768,7 @@ def swap_question_with_answer(request): """ if request.user.is_authenticated(): if request.user.is_administrator() or request.user.is_moderator(): - answer = models.Post.objects.get_answers().get(id = request.POST['answer_id']) + answer = models.Post.objects.get_answers(request.user).get(id = request.POST['answer_id']) new_question = answer.swap_with_question(new_title = request.POST['new_title']) return { 'id': new_question.id, diff --git a/askbot/views/readers.py b/askbot/views/readers.py index d27846e3..7782c7e6 100644 --- a/askbot/views/readers.py +++ b/askbot/views/readers.py @@ -73,10 +73,15 @@ def questions(request, **kwargs): if request.method != 'GET': return HttpResponseNotAllowed(['GET']) - search_state = SearchState(user_logged_in=request.user.is_authenticated(), **kwargs) + search_state = SearchState( + user_logged_in=request.user.is_authenticated(), + **kwargs + ) page_size = int(askbot_settings.DEFAULT_QUESTIONS_PAGE_SIZE) - qs, meta_data = models.Thread.objects.run_advanced_search(request_user=request.user, search_state=search_state) + qs, meta_data = models.Thread.objects.run_advanced_search( + request_user=request.user, search_state=search_state + ) if meta_data['non_existing_tags']: search_state = search_state.remove_tags(meta_data['non_existing_tags']) @@ -85,19 +90,25 @@ def questions(request, **kwargs): if paginator.num_pages < search_state.page: search_state.page = 1 page = paginator.page(search_state.page) - - page.object_list = list(page.object_list) # evaluate queryset + page.object_list = list(page.object_list) # evaluate the queryset # INFO: Because for the time being we need question posts and thread authors # down the pipeline, we have to precache them in thread objects models.Thread.objects.precache_view_data_hack(threads=page.object_list) - related_tags = Tag.objects.get_related_to_search(threads=page.object_list, ignored_tag_names=meta_data.get('ignored_tag_names', [])) + related_tags = Tag.objects.get_related_to_search( + threads=page.object_list, + ignored_tag_names=meta_data.get('ignored_tag_names',[]) + ) tag_list_type = askbot_settings.TAG_LIST_FORMAT if tag_list_type == 'cloud': #force cloud to sort by name related_tags = sorted(related_tags, key = operator.attrgetter('name')) - contributors = list(models.Thread.objects.get_thread_contributors(thread_list=page.object_list).only('id', 'username', 'gravatar')) + contributors = list( + models.Thread.objects.get_thread_contributors( + thread_list=page.object_list + ).only('id', 'username', 'gravatar') + ) paginator_context = { 'is_paginated' : (paginator.count > page_size), @@ -433,8 +444,11 @@ def question(request, id):#refactor - long subroutine. display question body, an #posts are pre-stuffed with the correctly ordered comments updated_question_post, answers, post_to_author = thread.get_cached_post_data( sort_method = answer_sort_method, + user = request.user ) - question_post.set_cached_comments(updated_question_post.get_cached_comments()) + question_post.set_cached_comments( + updated_question_post.get_cached_comments() + ) #Post.objects.precache_comments(for_posts=[question_post] + answers, visitor=request.user) @@ -539,7 +553,7 @@ def question(request, id):#refactor - long subroutine. display question body, an 'thread': thread, 'answer' : answer_form, 'answers' : page_objects.object_list, - 'answer_count': len(answers), + 'answer_count': thread.get_answer_count(request.user), 'category_tree_data': askbot_settings.CATEGORY_TREE, 'user_votes': user_votes, 'user_post_id_list': user_post_id_list, diff --git a/askbot/views/users.py b/askbot/views/users.py index 9d9419e1..d8e0fb6e 100644 --- a/askbot/views/users.py +++ b/askbot/views/users.py @@ -341,14 +341,19 @@ def user_stats(request, user, context): # # Top answers # - top_answers = user.posts.get_answers().filter( + top_answers = user.posts.get_answers( + request.user + ).filter( deleted=False, thread__posts__deleted=False, thread__posts__post_type='question', - ).select_related('thread').order_by('-score', '-added_at')[:100] + ).select_related( + 'thread' + ).order_by( + '-score', '-added_at' + )[:100] top_answer_count = len(top_answers) - # # Votes # diff --git a/askbot/views/writers.py b/askbot/views/writers.py index b5beb65b..dee1ee7b 100644 --- a/askbot/views/writers.py +++ b/askbot/views/writers.py @@ -217,6 +217,7 @@ def ask(request):#view used to ask a new question tagnames = form.cleaned_data['tags'] text = form.cleaned_data['text'] ask_anonymously = form.cleaned_data['ask_anonymously'] + post_privately = form.cleaned_data['post_privately'] if request.user.is_authenticated(): try: @@ -226,6 +227,7 @@ def ask(request):#view used to ask a new question tags = tagnames, wiki = wiki, is_anonymous = ask_anonymously, + is_private = post_privately, timestamp = timestamp ) return HttpResponseRedirect(question.get_absolute_url()) @@ -258,7 +260,8 @@ def ask(request):#view used to ask a new question 'text': request.REQUEST.get('text', ''), 'tags': request.REQUEST.get('tags', ''), 'wiki': request.REQUEST.get('wiki', False), - 'is_anonymous': request.REQUEST.get('is_anonymous', False), + 'ask_anonymously': request.REQUEST.get('ask_anonymousy', False), + 'post_privately': request.REQUEST.get('post_privately', False) } data = { @@ -379,6 +382,7 @@ def edit_question(request, id): is_anon_edit = form.cleaned_data['stay_anonymous'] is_wiki = form.cleaned_data.get('wiki', question.wiki) + post_privately = form.cleaned_data['post_privately'] request.user.edit_question( question = question, @@ -388,15 +392,21 @@ def edit_question(request, id): tags = form.cleaned_data['tags'], wiki = is_wiki, edit_anonymously = is_anon_edit, + is_private = post_privately ) return HttpResponseRedirect(question.get_absolute_url()) else: #request type was "GET" revision_form = forms.RevisionForm(question, latest_revision) + initial = { + 'post_privately': question.is_private(), + 'wiki': question.wiki + } form = forms.EditQuestionForm( question = question, revision = latest_revision, - user = request.user + user = request.user, + initial = initial ) data = { @@ -458,12 +468,15 @@ def edit_answer(request, id): body_text = form.cleaned_data['text'], revision_comment = form.cleaned_data['summary'], wiki = form.cleaned_data.get('wiki', answer.wiki), + is_private = form.cleaned_data.get('is_private', False) #todo: add wiki field to form ) return HttpResponseRedirect(answer.get_absolute_url()) else: revision_form = forms.RevisionForm(answer, latest_revision) form = forms.EditAnswerForm(answer, latest_revision) + if request.user.can_make_group_private_posts(): + form.initial['post_privately'] = answer.is_private() data = { 'page_class': 'edit-answer-page', 'active_tab': 'questions', @@ -499,11 +512,13 @@ def answer(request, id):#process a new answer if request.user.is_authenticated(): try: follow = form.cleaned_data['email_notify'] + is_private = form.cleaned_data['post_privately'] answer = request.user.post_answer( question = question, body_text = text, follow = follow, wiki = wiki, + is_private = is_private, timestamp = update_time, ) return HttpResponseRedirect(answer.get_absolute_url()) |