diff options
24 files changed, 293 insertions, 30 deletions
diff --git a/askbot/doc/source/changelog.rst b/askbot/doc/source/changelog.rst index 600c1cb4..b312d2f7 100644 --- a/askbot/doc/source/changelog.rst +++ b/askbot/doc/source/changelog.rst @@ -10,6 +10,7 @@ Development version * Added optional support for unicode slugs (Evgeny) * Optionally allow limiting one answer per question per person (Evgeny) * Added management command `build_livesettings_cache` (Adolfo) +* Administrators can post under fictional user accounts without logging out (jtrain, Evgeny) * Welcome email for the case when replying by email is enabled (Evgeny) * Detection of email signature based on the response to the welcome email (Evgeny) * Hide "website" and "about" section of the blocked user profiles diff --git a/askbot/doc/source/contributors.rst b/askbot/doc/source/contributors.rst index 69348b84..ce8656ea 100644 --- a/askbot/doc/source/contributors.rst +++ b/askbot/doc/source/contributors.rst @@ -41,6 +41,7 @@ Programming and documentation * Silvio Heuberger * `Alexandros <https://github.com/alexandros-z>`_ * `Paul Backhouse <https://github.com/powlo>`_ +* `jtrain <https://github.com/jtrain>`_ Translations ------------ diff --git a/askbot/forms.py b/askbot/forms.py index fe65cd02..2cdf1c5d 100644 --- a/askbot/forms.py +++ b/askbot/forms.py @@ -4,6 +4,7 @@ import re from django import forms from askbot import const from askbot.const import message_keys +from django.forms.util import ErrorList from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ungettext_lazy, string_concat from django.utils.text import get_text_list @@ -769,7 +770,80 @@ class DraftAnswerForm(forms.Form): text = forms.CharField(required=False) -class AskForm(PostPrivatelyForm): +class PostAsSomeoneForm(forms.Form): + post_author_username = forms.CharField( + initial=_('User name:'), + help_text=_( + 'Enter name to post on behalf of someone else. ' + 'Can create new accounts.' + ), + required=False, + widget=forms.TextInput(attrs={'class': 'tipped-input'}) + ) + post_author_email = forms.CharField( + initial=_('Email address:'), + required=False, + widget=forms.TextInput(attrs={'class': 'tipped-input'}) + ) + + def get_post_user(self, user): + """returns user on whose behalf the post or a revision + is being made + """ + username = self.cleaned_data['post_author_username'] + email= self.cleaned_data['post_author_email'] + if user.is_administrator() and username and email: + post_user = user.get_or_create_fake_user(username, email) + else: + post_user = user + return post_user + + def clean_post_author_username(self): + """if value is the same as initial, it is reset to + empty string + todo: maybe better to have field where initial value is invalid, + then we would not have to have two almost identical clean functions? + """ + username = self.cleaned_data.get('post_author_username', '') + initial_username = unicode(self.fields['post_author_username'].initial) + if username == initial_username: + self.cleaned_data['post_author_username'] = '' + return self.cleaned_data['post_author_username'] + + def clean_post_author_email(self): + """if value is the same as initial, it is reset to + empty string""" + email = self.cleaned_data.get('post_author_email', '') + initial_email = unicode(self.fields['post_author_email'].initial) + if email == initial_email: + email = '' + if email != '': + email = forms.EmailField().clean(email) + self.cleaned_data['post_author_email'] = email + return email + + def clean(self): + """requires email address if user name is given""" + username = self.cleaned_data.get('post_author_username', '') + email = self.cleaned_data.get('post_author_email', '') + if username == '' and email: + username_errors = self._errors.get( + 'post_author_username', + ErrorList() + ) + username_errors.append(_('User name is required with the email')) + self._errors['post_author_username'] = username_errors + raise forms.ValidationError('missing user name') + elif email == '' and username: + email_errors = self._errors.get('post_author_email', ErrorList()) + email_errors.append(_('Email is required if user name is added')) + self._errors['post_author_email'] = email_errors + raise forms.ValidationError('missing email') + + return self.cleaned_data + + +class AskForm(PostAsSomeoneForm, 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 @@ -794,14 +868,6 @@ class AskForm(PostPrivatelyForm): 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}) - ) def __init__(self, *args, **kwargs): super(AskForm, self).__init__(*args, **kwargs) @@ -904,21 +970,13 @@ class AskByEmailForm(forms.Form): return self.cleaned_data['subject'] -class AnswerForm(PostPrivatelyForm): +class AnswerForm(PostAsSomeoneForm, PostPrivatelyForm): text = AnswerEditorField() wiki = WikiField() 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}) - ) email_notify = EmailNotifyField(initial=False) def __init__(self, *args, **kwargs): @@ -989,7 +1047,7 @@ class RevisionForm(forms.Form): self.fields['revision'].initial = latest_revision.revision -class EditQuestionForm(PostPrivatelyForm): +class EditQuestionForm(PostAsSomeoneForm, PostPrivatelyForm): title = TitleField() text = QuestionEditorField() tags = TagNamesField() @@ -1099,6 +1157,7 @@ class EditQuestionForm(PostPrivatelyForm): field edit_anonymously. It relies on correct cleaning if the "reveal_identity" field """ + super(EditQuestionForm, self).clean() reveal_identity = self.cleaned_data.get('reveal_identity', False) stay_anonymous = False if reveal_identity is False and self.can_stay_anonymous(): @@ -1107,7 +1166,7 @@ class EditQuestionForm(PostPrivatelyForm): return self.cleaned_data -class EditAnswerForm(PostPrivatelyForm): +class EditAnswerForm(PostAsSomeoneForm, PostPrivatelyForm): text = AnswerEditorField() summary = SummaryField() wiki = WikiField() diff --git a/askbot/migrations/0126_add_field__auth_user__is_fake.py b/askbot/migrations/0126_add_field__auth_user__is_fake.py new file mode 100644 index 00000000..e0928ed7 --- /dev/null +++ b/askbot/migrations/0126_add_field__auth_user__is_fake.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from south.db import db +from south.v2 import SchemaMigration + +class Migration(SchemaMigration): + + def forwards(self, orm): + try: + # Adding field 'User.is_fake' + db.add_column( + u'auth_user', 'is_fake', + self.gf('django.db.models.fields.BooleanField')(default=False), keep_default=False) + except: + pass + + def backwards(self, orm): + db.delete_column('auth_user', 'is_fake') + + complete_apps = ['askbot'] diff --git a/askbot/migrations/0126_save_category_tree_as_json.py b/askbot/migrations/0127_save_category_tree_as_json.py index b13cd2fe..b13cd2fe 100644 --- a/askbot/migrations/0126_save_category_tree_as_json.py +++ b/askbot/migrations/0127_save_category_tree_as_json.py diff --git a/askbot/migrations/0127_add_groups_field__to__thread_and_post.py b/askbot/migrations/0128_add_groups_field__to__thread_and_post.py index ab8bf919..ab8bf919 100644 --- a/askbot/migrations/0127_add_groups_field__to__thread_and_post.py +++ b/askbot/migrations/0128_add_groups_field__to__thread_and_post.py diff --git a/askbot/migrations/0128_auto__del_field_post_is_private.py b/askbot/migrations/0129_auto__del_field_post_is_private.py index 68637bdc..68637bdc 100644 --- a/askbot/migrations/0128_auto__del_field_post_is_private.py +++ b/askbot/migrations/0129_auto__del_field_post_is_private.py diff --git a/askbot/migrations/0129_auto__del_field_postrevision_revision_type.py b/askbot/migrations/0130_auto__del_field_postrevision_revision_type.py index 4143ffa8..4143ffa8 100644 --- a/askbot/migrations/0129_auto__del_field_postrevision_revision_type.py +++ b/askbot/migrations/0130_auto__del_field_postrevision_revision_type.py diff --git a/askbot/migrations/0130_auto__add_field_tag_status.py b/askbot/migrations/0131_auto__add_field_tag_status.py index f5557546..f5557546 100644 --- a/askbot/migrations/0130_auto__add_field_tag_status.py +++ b/askbot/migrations/0131_auto__add_field_tag_status.py diff --git a/askbot/migrations/0131_auto__add_draftquestion__add_draftanswer.py b/askbot/migrations/0132_auto__add_draftquestion__add_draftanswer.py index bceca754..bceca754 100644 --- a/askbot/migrations/0131_auto__add_draftquestion__add_draftanswer.py +++ b/askbot/migrations/0132_auto__add_draftquestion__add_draftanswer.py diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py index 4fbabe9d..10a8aa03 100644 --- a/askbot/models/__init__.py +++ b/askbot/models/__init__.py @@ -89,6 +89,7 @@ User.add_to_class( choices = const.USER_STATUS_CHOICES ) ) +User.add_to_class('is_fake', models.BooleanField(default=False)) User.add_to_class('email_isvalid', models.BooleanField(default=False)) #@UndefinedVariable User.add_to_class('email_key', models.CharField(max_length=32, null=True)) @@ -356,6 +357,24 @@ def user_can_post_by_email(self): return askbot_settings.REPLY_BY_EMAIL and \ self.reputation > askbot_settings.MIN_REP_TO_POST_BY_EMAIL +def user_get_or_create_fake_user(self, username, email): + """ + Get's or creates a user, most likely with the purpose + of posting under that account. + """ + assert(self.is_administrator()) + + try: + user = User.objects.get(username=username) + except User.DoesNotExist: + user = User() + user.username = username + user.email = email + user.is_fake = True + user.set_unusable_password() + user.save() + return user + def _assert_user_can( user = None, post = None, #related post (may be parent) @@ -2579,6 +2598,7 @@ 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_or_create_fake_user', user_get_or_create_fake_user) User.add_to_class('get_marked_tags', user_get_marked_tags) User.add_to_class('get_marked_tag_names', user_get_marked_tag_names) User.add_to_class('get_groups', user_get_groups) diff --git a/askbot/skins/common/media/js/post.js b/askbot/skins/common/media/js/post.js index e8d521ed..debae9b7 100644 --- a/askbot/skins/common/media/js/post.js +++ b/askbot/skins/common/media/js/post.js @@ -3807,6 +3807,33 @@ $(document).ready(function() { questionRetagger.init(); } socialSharing.init(); + + var proxyUserNameInput = $('#id_post_author_username'); + var proxyUserEmailInput = $('#id_post_author_email'); + if (proxyUserNameInput.length === 1) { + var tip = new TippedInput(); + tip.decorate(proxyUserNameInput); + + var userSelectHandler = function(data) { + proxyUserEmailInput.val(data['data'][0]); + }; + + var fakeUserAc = new AutoCompleter({ + url: '/get-users-info/',//askbot['urls']['get_users_info'], + preloadData: true, + minChars: 1, + useCache: true, + matchInside: true, + maxCacheLength: 100, + delay: 10, + onItemSelect: userSelectHandler + }); + fakeUserAc.decorate(proxyUserNameInput); + } + if (proxyUserEmailInput.length === 1) { + var tip = new TippedInput(); + tip.decorate(proxyUserEmailInput); + } }); diff --git a/askbot/skins/common/media/js/utils.js b/askbot/skins/common/media/js/utils.js index 30d2a64e..01239304 100644 --- a/askbot/skins/common/media/js/utils.js +++ b/askbot/skins/common/media/js/utils.js @@ -355,6 +355,7 @@ TippedInput.prototype.decorate = function(element){ this._element = element; var instruction_text = this.getVal(); this._instruction = instruction_text; + this.reset(); var me = this; $(element).focus(function(){ if (me.isBlank()){ diff --git a/askbot/skins/common/templates/widgets/edit_post.html b/askbot/skins/common/templates/widgets/edit_post.html index 12e1d25f..1663c7d4 100644 --- a/askbot/skins/common/templates/widgets/edit_post.html +++ b/askbot/skins/common/templates/widgets/edit_post.html @@ -69,6 +69,7 @@ <div class="form-error" >{{ post_form.summary.errors }}</div> </div> {% endif %} + {% if editor_type == 'markdown' %} <div class="preview-toggle"> <span @@ -80,3 +81,31 @@ </div> <div id="previewer" class="wmd-preview"></div> {% endif %} + +{% if user and user.is_authenticated() and user.is_administrator() %} + {# admin can post answers or questions on behalf of anyone. #} + <table class="proxy-user-info"> + <tbody> + <tr><td colspan="2"> + <label> + {% trans %}To post on behalf of someone else, enter user name <strong>and</strong> email below.{% endtrans %} + </label> + </td></tr> + <tr> + <td> + <div class="form-item"> + {{ post_form.post_author_username }} + </div> + <div class="form-item"> + {{ post_form.post_author_email }} + </div> + </td> + </tr> + <tr> + <td colspan="2"> + <span class="form-error">{{ post_form.post_author_username.errors }}</span> + <span class="form-error">{{ post_form.post_author_email.errors }}</span> + </td> + </tbody> + </table> +{% endif %} diff --git a/askbot/skins/default/media/style/style.css b/askbot/skins/default/media/style/style.css index 7a695825..c8d92f60 100644 --- a/askbot/skins/default/media/style/style.css +++ b/askbot/skins/default/media/style/style.css @@ -1358,7 +1358,9 @@ ul#related-tags li { font-size: 13px; } .ask-page #id_tags, -.edit-question-page #id_tags { +.edit-question-page #id_tags, +#id_user, +#id_user_author { border: #cce6ec 3px solid; height: 25px; padding-left: 5px; diff --git a/askbot/skins/default/media/style/style.less b/askbot/skins/default/media/style/style.less index 1116b205..c2fcb3d3 100644 --- a/askbot/skins/default/media/style/style.less +++ b/askbot/skins/default/media/style/style.less @@ -1403,13 +1403,46 @@ ul#related-tags li { font-size:13px; } - #id_tags{ + #id_tags { border:#cce6ec 3px solid; height:25px; padding-left:5px; + font-size:14px; width:395px; + } +} + +.ask-page, +.question-page, +.edit-question-page, +.edit-answer-page { + #id_post_author_username, + #id_post_author_email { + border:#cce6ec 3px solid; + height:25px; + padding-left:5px; font-size:14px; + width:186px; } + #id_post_author_email { + margin-left: 10px; + } + table.proxy-user-info { + border-spacing: 0px; + + .form-item { + float: left; + } + } +} + +#id_user, +#id_user_author { + border:#cce6ec 3px solid; + height:25px; + padding-left:5px; + width:395px; + font-size:14px; } .title-desc { diff --git a/askbot/skins/default/templates/macros.html b/askbot/skins/default/templates/macros.html index 4204d362..59bde754 100644 --- a/askbot/skins/default/templates/macros.html +++ b/askbot/skins/default/templates/macros.html @@ -483,7 +483,8 @@ for the purposes of the AJAX comment editor #} edit_title = False, use_category_selector = False, tag_names = None, - editor_type = None + editor_type = None, + user = None ) -%} {%include "widgets/edit_post.html" %} diff --git a/askbot/skins/default/templates/question/new_answer_form.html b/askbot/skins/default/templates/question/new_answer_form.html index c590b469..9c0258f7 100644 --- a/askbot/skins/default/templates/question/new_answer_form.html +++ b/askbot/skins/default/templates/question/new_answer_form.html @@ -39,7 +39,12 @@ {% endif %} </p> {% endif %} - {{ macros.edit_post(answer, editor_type = settings.EDITOR_TYPE) }} + {{ macros.edit_post( + answer, + user = request.user, + editor_type = settings.EDITOR_TYPE + ) + }} <input id="add-answer-btn" type="submit" class="submit after-editor" style="float:left"/> <script type="text/javascript"> askbot['functions']['renderAddAnswerButton'](); diff --git a/askbot/skins/default/templates/question_edit.html b/askbot/skins/default/templates/question_edit.html index a6efe913..f4e4fbbf 100644 --- a/askbot/skins/default/templates/question_edit.html +++ b/askbot/skins/default/templates/question_edit.html @@ -26,7 +26,8 @@ mandatory_tags = mandatory_tags, use_category_selector = use_category_selector, tag_names = tag_names, - editor_type = settings.EDITOR_TYPE + editor_type = settings.EDITOR_TYPE, + user = request.user ) }} <div class="after-editor"> diff --git a/askbot/skins/default/templates/widgets/ask_form.html b/askbot/skins/default/templates/widgets/ask_form.html index eaa3e154..d528609f 100644 --- a/askbot/skins/default/templates/widgets/ask_form.html +++ b/askbot/skins/default/templates/widgets/ask_form.html @@ -27,7 +27,8 @@ edit_title = False, mandatory_tags = mandatory_tags, use_category_selector = (settings.TAG_SOURCE == 'category-tree'), - editor_type = settings.EDITOR_TYPE + editor_type = settings.EDITOR_TYPE, + user = request.user ) }} {{ form.group_id }} diff --git a/askbot/tests/form_tests.py b/askbot/tests/form_tests.py index 654272b3..a37f380c 100644 --- a/askbot/tests/form_tests.py +++ b/askbot/tests/form_tests.py @@ -334,3 +334,30 @@ class AnswerEditorFieldTests(AskbotTestCase): self.field.clean(10*'a'), 10*'a' ) + + +class PostAsSomeoneFormTests(AskbotTestCase): + + form = forms.PostAsSomeoneForm + + def setUp(self): + self.good_data = { + 'username': 'me', + 'email': 'me@example.com' + } + + def test_blank_form_validates(self): + form = forms.PostAsSomeoneForm({}) + self.assertEqual(form.is_valid(), True) + + def test_complete_form_validates(self): + form = forms.PostAsSomeoneForm(self.good_data) + self.assertEqual(form.is_valid(), True) + + def test_missing_email_fails(self): + form = forms.PostAsSomeoneForm({'post_author_username': 'me'}) + self.assertEqual(form.is_valid(), False) + + def test_missing_username_fails(self): + form = forms.PostAsSomeoneForm({'post_author_email': 'me@example.com'}) + self.assertEqual(form.is_valid(), False) diff --git a/askbot/urls.py b/askbot/urls.py index c7c9c9f8..98c6810d 100644 --- a/askbot/urls.py +++ b/askbot/urls.py @@ -89,6 +89,11 @@ urlpatterns = patterns('', name = 'save_draft_answer' ), url( + r'^get-users-info/', + views.commands.get_users_info, + name='get_users_info' + ), + url( r'^%s%s$' % (_('questions/'), _('ask/')), views.writers.ask, name='ask' diff --git a/askbot/views/commands.py b/askbot/views/commands.py index 0757aa6d..cc7be5f1 100644 --- a/askbot/views/commands.py +++ b/askbot/views/commands.py @@ -1113,3 +1113,25 @@ def save_draft_answer(request): draft.thread = thread draft.text = form.cleaned_data.get('text', '') draft.save() + +@decorators.get_only +@decorators.admins_only +def get_users_info(request): + """retuns list of user names and email addresses + of "fake" users - so that admins can post on their + behalf""" + user_info_list = models.User.objects.filter( + is_fake=True + ).values_list( + 'username', + 'email' + ) + + result_list = list() + for user_info in user_info_list: + username = user_info[0] + email = user_info[1] + result_list.append('%s|%s' % (username, email)) + + output = '\n'.join(result_list) + return HttpResponse(output, mimetype = 'text/plain') diff --git a/askbot/views/writers.py b/askbot/views/writers.py index 5dc1786e..04396cff 100644 --- a/askbot/views/writers.py +++ b/askbot/views/writers.py @@ -15,6 +15,7 @@ import time import urlparse from django.shortcuts import get_object_or_404 from django.contrib.auth.decorators import login_required +from django.contrib.auth.models import User from django.http import HttpResponseRedirect, HttpResponse, HttpResponseForbidden, Http404 from django.utils import simplejson from django.utils.html import strip_tags, escape @@ -226,8 +227,10 @@ def ask(request):#view used to ask a new question author=request.user ) drafts.delete() + + user = form.get_post_user(request.user) try: - question = request.user.post_question( + question = user.post_question( title = title, body_text = text, tags = tagnames, @@ -411,7 +414,9 @@ def edit_question(request, id): is_wiki = form.cleaned_data.get('wiki', question.wiki) post_privately = form.cleaned_data['post_privately'] - request.user.edit_question( + user = form.get_post_user(request.user) + + user.edit_question( question = question, title = form.cleaned_data['title'], body_text = form.cleaned_data['text'], @@ -487,7 +492,8 @@ def edit_answer(request, id): if form.is_valid(): if form.has_changed(): - request.user.edit_answer( + user = form.get_post_user(request.user) + user.edit_answer( answer = answer, body_text = form.cleaned_data['text'], revision_comment = form.cleaned_data['summary'], @@ -543,7 +549,10 @@ def answer(request, id):#process a new answer try: follow = form.cleaned_data['email_notify'] is_private = form.cleaned_data['post_privately'] - answer = request.user.post_answer( + + user = form.get_post_user(request.user) + + answer = user.post_answer( question = question, body_text = text, follow = follow, |