diff options
author | Evgeny Fadeev <evgeny.fadeev@gmail.com> | 2014-07-30 02:55:47 -0300 |
---|---|---|
committer | Evgeny Fadeev <evgeny.fadeev@gmail.com> | 2014-07-30 02:55:47 -0300 |
commit | a807440966f9bb5c3179cb3defff89e90e8f7845 (patch) | |
tree | 6eecea205ab249bea304459693b8c0dd012b2e4d | |
parent | 258537d05f2db65454782df449c79148de7f8310 (diff) | |
parent | a848169319062d37b65fd93bed38bdf948c07322 (diff) | |
download | askbot-a807440966f9bb5c3179cb3defff89e90e8f7845.tar.gz askbot-a807440966f9bb5c3179cb3defff89e90e8f7845.tar.bz2 askbot-a807440966f9bb5c3179cb3defff89e90e8f7845.zip |
merge duplicate questions feature
-rw-r--r-- | askbot/conf/words.py | 18 | ||||
-rw-r--r-- | askbot/doc/source/changelog.rst | 1 | ||||
-rw-r--r-- | askbot/media/images/merge.png | bin | 0 -> 380 bytes | |||
-rw-r--r-- | askbot/media/js/post.js | 160 | ||||
-rw-r--r-- | askbot/media/js/utils.js | 44 | ||||
-rw-r--r-- | askbot/media/style/style.css | 24 | ||||
-rw-r--r-- | askbot/media/style/style.less | 33 | ||||
-rw-r--r-- | askbot/models/__init__.py | 42 | ||||
-rw-r--r-- | askbot/models/post.py | 36 | ||||
-rw-r--r-- | askbot/models/question.py | 5 | ||||
-rw-r--r-- | askbot/templates/question.html | 31 | ||||
-rw-r--r-- | askbot/templates/question/answer_controls.html | 2 | ||||
-rw-r--r-- | askbot/templates/question/question_controls.html | 1 | ||||
-rw-r--r-- | askbot/tests/api_v1_tests.py | 2 | ||||
-rw-r--r-- | askbot/urls.py | 5 | ||||
-rw-r--r-- | askbot/views/api_v1.py | 4 | ||||
-rw-r--r-- | askbot/views/commands.py | 17 |
17 files changed, 407 insertions, 18 deletions
diff --git a/askbot/conf/words.py b/askbot/conf/words.py index 5e12b111..bb99bbce 100644 --- a/askbot/conf/words.py +++ b/askbot/conf/words.py @@ -140,6 +140,24 @@ settings.register( settings.register( values.StringValue( WORDS, + 'WORDS_MERGE_QUESTIONS', + default=_('Merge duplicate questions'), + description=_('Merge duplicate questions') + ) +) + +settings.register( + values.StringValue( + WORDS, + 'WORDS_ENTER_DUPLICATE_QUESTION_ID', + default=_('Enter duplicate question ID'), + description=_('Enter duplicate question ID') + ) +) + +settings.register( + values.StringValue( + WORDS, 'WORDS_ASKED', default=_('asked'), description=_('asked'), diff --git a/askbot/doc/source/changelog.rst b/askbot/doc/source/changelog.rst index 308ee4c6..177d9018 100644 --- a/askbot/doc/source/changelog.rst +++ b/askbot/doc/source/changelog.rst @@ -3,6 +3,7 @@ Changes in Askbot Development master branch (only on github) ------------------------------------------ +* Admins and Moderators can merge questions. * Improved moderation modes: flags, audit, premoderation. Watched user status, IP blocking, mass content removal. * Allow bulk deletion of user content simultaneously with blocking diff --git a/askbot/media/images/merge.png b/askbot/media/images/merge.png Binary files differnew file mode 100644 index 00000000..c3edac7b --- /dev/null +++ b/askbot/media/images/merge.png diff --git a/askbot/media/js/post.js b/askbot/media/js/post.js index 2efe0bf8..b2c596f8 100644 --- a/askbot/media/js/post.js +++ b/askbot/media/js/post.js @@ -242,6 +242,157 @@ ThreadUsersDialog.prototype.decorate = function(element) { }); }; +var MergeQuestionsDialog = function() { + ModalDialog.call(this); + this._tags = []; + this._prevQuestionId = undefined; +}; +inherits(MergeQuestionsDialog, ModalDialog); + +MergeQuestionsDialog.prototype.show = function() { + MergeQuestionsDialog.superClass_.show.call(this); + this._idInput.focus(); +}; + +MergeQuestionsDialog.prototype.getStartMergingHandler = function() { + var me = this; + return function() { + $.ajax({ + type: 'POST', + cache: false, + dataType: 'json', + url: askbot['urls']['mergeQuestions'], + data: JSON.stringify({ + from_id: me.getFromId(), + to_id: me.getToId() + }), + success: function(data) { + window.location.reload(); + } + }); + }; +}; + +MergeQuestionsDialog.prototype.setPreview = function(data) { + this._previewTitle.html(data['title']); + this._previewBody.html(data['summary']); + for (var i=0; i<this._tags.length; i++) { + this._tags[i].dispose(); + } + for (i=0; i<data['tags'].length; i++) { + var tag = new Tag(); + tag.setLinkable(false); + tag.setName(data['tags'][i]); + this._previewTags.append(tag.getElement()); + this._tags.push(tag); + } + this._preview.fadeIn(); +}; + +MergeQuestionsDialog.prototype.clearPreview = function() { + for (var i=0; i<this._tags.length; i++) { + this._tags[i].dispose(); + } + this._previewTitle.html(''); + this._previewBody.html(''); + this._previewTags.html(''); + this._preview.hide(); +}; + +MergeQuestionsDialog.prototype.getFromId = function() { + return this._fromId; +}; + +MergeQuestionsDialog.prototype.getToId = function() { + return this._idInput.val(); +}; + +MergeQuestionsDialog.prototype.getPrevToId = function() { + return this._prevQuestionId; +}; + +MergeQuestionsDialog.prototype.setPrevToId = function(toId) { + this._prevQuestionId = toId; +}; + +MergeQuestionsDialog.prototype.getLoadPreviewHandler = function() { + var me = this; + return function() { + var prevId = me.getPrevToId(); + var curId = me.getToId(); + if (curId && curId != prevId) { + $.ajax({ + type: 'GET', + cache: false, + dataType: 'json', + url: askbot['urls']['apiV1Questions'] + curId + '/', + success: function(data) { + me.setPreview(data); + me.setPrevToId(curId); + me.setAcceptButtonText(gettext('Merge')); + }, + error: function() { + me.clearPreview(); + me.setAcceptButtonText(gettext('Load preview')); + } + }); + } + }; +}; + +MergeQuestionsDialog.prototype.createDom = function() { + //make content + var content = this.makeElement('div'); + var label = this.makeElement('label'); + label.attr('for', 'question_id'); + label.html(gettext(askbot['messages']['enterDuplicateQuestionId'])); + content.append(label); + var input = this.makeElement('input'); + input.attr('type', 'text'); + input.attr('name', 'question_id'); + content.append(input); + this._idInput = input; + + var preview = this.makeElement('div'); + content.append(preview); + this._preview = preview; + preview.hide(); + + var title = this.makeElement('h3'); + preview.append(title); + this._previewTitle = title; + + var tags = this.makeElement('div'); + tags.addClass('tags'); + this._preview.append(tags); + this._previewTags = tags; + + var clr = this.makeElement('div'); + clr.addClass('clearfix'); + this._preview.append(clr); + + var body = this.makeElement('div'); + body.addClass('body'); + this._preview.append(body); + this._previewBody = body; + + var previewHandler = this.getLoadPreviewHandler(); + var enterHandler = makeKeyHandler(13, previewHandler); + input.keydown(enterHandler); + input.blur(previewHandler); + + this.setContent(content); + + this.setClass('merge-questions'); + this.setRejectButtonText(gettext('Cancel')); + this.setAcceptButtonText(gettext('Load preview')); + this.setHeadingText(askbot['messages']['mergeQuestions']); + this.setAcceptHandler(this.getStartMergingHandler()); + + MergeQuestionsDialog.superClass_.createDom.call(this); + + this._fromId = $('.post.question').data('postId'); +}; /** * @constructor @@ -4775,6 +4926,15 @@ $(document).ready(function() { var askButton = new AskButton(); askButton.decorate($("#askButton")); + + if (askbot['data']['userIsThreadModerator']) { + var mergeQuestions = new MergeQuestionsDialog(); + $(document).append(mergeQuestions.getElement()); + var mergeBtn = $('.question-merge'); + setupButtonEventHandlers(mergeBtn, function() { + mergeQuestions.show(); + }); + } }); /* google prettify.js from google code */ diff --git a/askbot/media/js/utils.js b/askbot/media/js/utils.js index 7d877b4b..f1c3e17e 100644 --- a/askbot/media/js/utils.js +++ b/askbot/media/js/utils.js @@ -1452,6 +1452,7 @@ DeleteIcon.prototype.setContent = function(content){ var ModalDialog = function() { WrappedElement.call(this); this._accept_button_text = gettext('Ok'); + this._acceptBtnEnabled = true; this._reject_button_text = gettext('Cancel'); this._heading_text = 'Add heading by setHeadingText()'; this._initial_content = undefined; @@ -1472,6 +1473,13 @@ ModalDialog.prototype.hide = function() { this._element.modal('hide'); }; +ModalDialog.prototype.setClass = function(cls) { + this._cssClass = cls; + if (this._element) { + this._element.addClass(cls); + }; +}; + ModalDialog.prototype.setContent = function(content) { this._initial_content = content; if (this._content_element) { @@ -1489,12 +1497,23 @@ ModalDialog.prototype.setHeadingText = function(text) { ModalDialog.prototype.setAcceptButtonText = function(text) { this._accept_button_text = text; + if (this._acceptBtn) { + this._acceptBtn.html(text); + } }; ModalDialog.prototype.setRejectButtonText = function(text) { this._reject_button_text = text; }; +ModalDialog.prototype.hideRejectButton = function() { + this._rejectBtn.hide(); +}; + +ModalDialog.prototype.hideAcceptButton = function() { + this._acceptButton.hide(); +}; + ModalDialog.prototype.setAcceptHandler = function(handler) { this._accept_handler = handler; }; @@ -1516,10 +1535,28 @@ ModalDialog.prototype.setMessage = function(text, message_type) { this.prependContent(box.getElement()); }; +ModalDialog.prototype.disableAcceptButton = function() { + this._acceptBtnEnabled = false; + if (this._acceptBtn) { + this._acceptBtn.prop('disabled', true); + } +}; + +ModalDialog.prototype.enableAcceptButton = function() { + this._acceptBtnEnabled = true; + if (this._acceptBtn) { + this._acceptBtn.prop('disabled', false); + } +}; + ModalDialog.prototype.createDom = function() { this._element = this.makeElement('div') var element = this._element; + if (this._cssClass) { + element.addClass(this._cssClass); + } + element.addClass('modal'); if (this._className) { element.addClass(this._className); @@ -1540,8 +1577,6 @@ ModalDialog.prototype.createDom = function() { header.append(title); } - - //2) create content var body = this.makeElement('div') body.addClass('modal-body'); @@ -1557,15 +1592,20 @@ ModalDialog.prototype.createDom = function() { element.append(footer); var accept_btn = this.makeElement('button'); + if (this._acceptBtnEnabled === false) { + accept_btn.prop('disabled', true); + } accept_btn.addClass('submit'); accept_btn.html(this._accept_button_text); footer.append(accept_btn); + this._acceptBtn = accept_btn; if (this._reject_button_text) { var reject_btn = this.makeElement('button'); reject_btn.addClass('submit cancel'); reject_btn.html(this._reject_button_text); footer.append(reject_btn); + this._rejectBtn = reject_btn; } //4) attach event handlers to the buttons diff --git a/askbot/media/style/style.css b/askbot/media/style/style.css index b9bb6643..8d46fb20 100644 --- a/askbot/media/style/style.css +++ b/askbot/media/style/style.css @@ -2291,6 +2291,10 @@ ul#related-tags li { .question-page .answer-controls .question-close { background: url(../images/close.png) no-repeat center left; } +.question-page .post-controls .question-merge, +.question-page .answer-controls .question-merge { + background: url(../images/merge.png) no-repeat; +} .question-page .post-controls .permant-link, .question-page .answer-controls .permant-link { background: url(../images/link.png) no-repeat 2px 1px; @@ -4305,9 +4309,29 @@ textarea.tipped-input { .modal-footer { text-align: left; } +.modal h3 { + padding: 0; +} .modal p { font-size: 14px; } +.modal.merge-questions .modal-body label { + padding-right: 8px; +} +.modal.merge-questions .modal-body .body, +.modal.merge-questions .modal-body .tags { + clear: both; +} +.modal.merge-questions .modal-body h3 { + line-height: 22px; + margin-top: 12px; +} +.modal.merge-questions .modal-body .tags { + margin-top: 12px; +} +.modal.merge-questions .modal-body .body { + margin-top: 16px; +} .modal-body > textarea { width: 515px; margin-bottom: 0px; diff --git a/askbot/media/style/style.less b/askbot/media/style/style.less index d508420c..285b33dc 100644 --- a/askbot/media/style/style.less +++ b/askbot/media/style/style.less @@ -2405,6 +2405,9 @@ ul#related-tags li { .question-close{ background: url(../images/close.png) no-repeat center left; } + .question-merge { + background: url(../images/merge.png) no-repeat; + } .permant-link{ background: url(../images/link.png) no-repeat 2px 1px; } @@ -4520,8 +4523,34 @@ textarea.tipped-input { .modal-footer { text-align: left; } -.modal p { - font-size: 14px; +.modal { + h3 { + padding: 0; + } + p { + font-size: 14px; + } +} +.modal.merge-questions { + .modal-body { + label { + padding-right: 8px; + } + .body, + .tags { + clear: both; + } + h3 { + line-height: 22px; + margin-top: 12px; + } + .tags { + margin-top: 12px; + } + .body { + margin-top: 16px; + } + } } .modal-body > textarea { width: 515px; diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py index dca5afb0..bc94c094 100644 --- a/askbot/models/__init__.py +++ b/askbot/models/__init__.py @@ -787,6 +787,14 @@ def user_assert_can_upload_file(request_user): ) +def user_assert_can_merge_questions(self): + _assert_user_can( + user=self, + action_display=_('merge duplicate questions'), + admin_or_moderator_required=True + ) + + def user_assert_can_post_text(self, text): """Raises exceptions.PermissionDenied, if user does not have privilege to post given text, depending on the contents @@ -1385,6 +1393,38 @@ def user_mark_tags( return cleaned_tagnames, cleaned_wildcards +def user_merge_duplicate_questions(self, from_q, to_q): + """merges content from the ``from_thread`` to the ``to-thread``""" + #todo: maybe assertion will depend on which questions are merged + self.assert_can_merge_questions() + to_q.merge_post(from_q) + from_thread = from_q.thread + to_thread = to_q.thread + #set new thread value to all posts + posts = from_thread.posts.all() + posts.update(thread=to_thread) + + if askbot_settings.LIMIT_ONE_ANSWER_PER_USER: + #merge answers if only one is allowed per user + answers = to_thread.all_answers() + answer_map = collections.defaultdict(list) + #compile all answers by user + for answer in answers: + author = answer.author + answer_map[author].append(answer) + + for author in answer_map: + author_answers = answer_map[author] + if author_answers > 1: + first_answer = author_answers.pop(0) + for answer in author_answers: + first_answer.merge_post(answer) + + from_thread.spaces.clear() + from_thread.delete() + to_thread.invalidate_cached_data() + + @auto_now_timestamp def user_retag_question( self, @@ -3018,6 +3058,7 @@ User.add_to_class('follow_question', user_follow_question) User.add_to_class('unfollow_question', user_unfollow_question) User.add_to_class('is_following_question', user_is_following_question) User.add_to_class('mark_tags', user_mark_tags) +User.add_to_class('merge_duplicate_questions', user_merge_duplicate_questions) User.add_to_class('update_response_counts', user_update_response_counts) User.add_to_class('can_create_tags', user_can_create_tags) User.add_to_class('can_have_strong_url', user_can_have_strong_url) @@ -3076,6 +3117,7 @@ User.add_to_class('is_read_only', user_is_read_only) User.add_to_class('assert_can_vote_for_post', user_assert_can_vote_for_post) User.add_to_class('assert_can_revoke_old_vote', user_assert_can_revoke_old_vote) User.add_to_class('assert_can_upload_file', user_assert_can_upload_file) +User.add_to_class('assert_can_merge_questions', user_assert_can_merge_questions) User.add_to_class('assert_can_post_question', user_assert_can_post_question) User.add_to_class('assert_can_post_answer', user_assert_can_post_answer) User.add_to_class('assert_can_post_comment', user_assert_can_post_comment) diff --git a/askbot/models/post.py b/askbot/models/post.py index 594e1d9f..7721b37b 100644 --- a/askbot/models/post.py +++ b/askbot/models/post.py @@ -788,6 +788,41 @@ class Post(models.Model): groups = (Group.objects.get_global_group(),) self.add_to_groups(groups) + def merge_post(self, post): + """merge with other post""" + #take latest revision of current post R1 + rev = self.get_latest_revision() + orig_text = rev.text + for rev in post.revisions.all().order_by('revision'): + #for each revision of other post Ri + #append content of Ri to R1 and use author + new_text = orig_text + '\n\n' + rev.text + author = rev.author + self.apply_edit( + edited_by=rev.author, + text=new_text, + comment=_('merged revision'), + by_email=False, + edit_anonymously=rev.is_anonymous, + suppress_email=True, + ip_addr=rev.ip_addr + ) + if post.is_question() or post.is_answer(): + comments = Post.objects.get_comments().filter(parent=post) + comments.update(parent=self) + + #todo: implement redirects + if post.is_question(): + self.old_question_id = post.id + elif post.is_answer(): + self.old_answer_id = post.id + elif post.is_comment(): + self.old_comment_id = post.id + + self.save() + post.delete() + + def is_private(self): """true, if post belongs to the global group""" if askbot_settings.GROUPS_ENABLED: @@ -1834,6 +1869,7 @@ class Post(models.Model): comment=None, wiki=False, is_private=False, + edit_anonymously=False, by_email=False, suppress_email=False, ip_addr=None, diff --git a/askbot/models/question.py b/askbot/models/question.py index 84a358e5..685c3c9e 100644 --- a/askbot/models/question.py +++ b/askbot/models/question.py @@ -980,11 +980,12 @@ class Thread(models.Model): """true if ``user`` is also a thread moderator""" if user.is_anonymous(): return False - elif askbot_settings.GROUPS_ENABLED: - if user.is_administrator_or_moderator(): + if user.is_administrator_or_moderator(): + if askbot_settings.GROUPS_ENABLED: user_groups = user.get_groups(private=True) thread_groups = self.get_groups_shared_with() return bool(set(user_groups) & set(thread_groups)) + return True return False def requires_response_moderation(self, author): diff --git a/askbot/templates/question.html b/askbot/templates/question.html index 10df6027..1557eb38 100644 --- a/askbot/templates/question.html +++ b/askbot/templates/question.html @@ -13,7 +13,7 @@ <script type="text/javascript"> /*<![CDATA[*/ //below is pure cross-browser javascript, no jQuery - askbot['data']['userIsThreadModerator'] = {% if user_is_thread_moderator %}true{% else %}false{% endif %}; + askbot['data']['userIsThreadModerator'] = {{ user_is_thread_moderator|as_js_bool }}; askbot['data']['oldestAnswerId'] = {% if oldest_answer_id %}{{ oldest_answer_id }}{% else %}-1{% endif %}; {% if settings.READ_ONLY_MODE_ENABLED %} askbot['settings']['readOnlyModeEnabled'] = true; @@ -173,7 +173,9 @@ //hide publish/unpublish answer links var answerId = 'post-' + postId + '-publish'; var pubBtn = document.getElementById(answerId); - pubBtn.parentNode.removeChild(pubBtn); + if (pubBtn) { + removeNode(pubBtn); + } } } @@ -208,6 +210,13 @@ return; } + var deleteBtn = document.getElementById('post-' + post_id + '-delete'); + var controls = deleteBtn.parentNode; + var mergeBtn = findChildrenByClassName(controls, 'question-merge'); + if (mergeBtn.length === 1 && data['userIsThreadModerator'] === false) { + removeNode(mergeBtn[0]); + } + if (data['userIsAdminOrMod']){ return;//all remaining functions stay on } @@ -215,12 +224,9 @@ //todo: remove edit button from older comments return; } - var deleteBtn = document.getElementById('post-' + post_id + '-delete'); - var controls = deleteBtn.parentNode; if (//maybe remove "delete" button (data['userReputation'] < - {{settings.MIN_REP_TO_DELETE_OTHERS_COMMENTS}}) || - data['userIsReadOnly'] + {{settings.MIN_REP_TO_DELETE_OTHERS_COMMENTS}}) || data['userIsReadOnly'] ) { removeNode(deleteBtn); } @@ -240,6 +246,7 @@ ) { removeNode(closeBtn[0]); } + var repLow = (data['userReputation'] < {{settings.MIN_REP_TO_EDIT_OTHERS_POSTS}}); if (//maybe remove "edit" button repLow || data['userIsReadOnly']//only authors edit comments @@ -361,15 +368,19 @@ askbot['urls']['get_html_template'] = '{% url get_html_template %}'; askbot['urls']['getGroupsList'] = '{% url get_groups_list %}'; askbot['urls']['publishAnswer'] = '{% url publish_answer %}'; - askbot['data']['userIsThreadModerator'] = {{ user_is_thread_moderator|as_js_bool }}; + askbot['urls']['apiV1Questions'] = '{% url api_v1_questions %}'; + askbot['urls']['mergeQuestions'] = '{% url merge_questions %}'; + {# already added on top askbot['data']['userIsThreadModerator'] = {{ user_is_thread_moderator|as_js_bool }}; #} askbot['data']['questionAuthorId'] = {{ question.author_id }}; askbot['data']['threadIsClosed'] = {{ thread.closed|as_js_bool }}; askbot['data']['answersSortTab'] = '{{ tab_id }}'; askbot['data']['questionId'] = {{ question.id }}; askbot['data']['threadSlug'] = '{{ thread.title|slugify }}'; - askbot['messages']['addComment'] = "{% trans %}add a comment{% endtrans %}"; - askbot['messages']['userNamePrompt'] = "{% trans %}User name:{% endtrans %}"; - askbot['messages']['userEmailPrompt'] = "{% trans %}Email address:{% endtrans %}"; + askbot['messages']['addComment'] = '{% trans %}add a comment{% endtrans %}'; + askbot['messages']['userNamePrompt'] = '{% trans %}User name:{% endtrans %}'; + askbot['messages']['userEmailPrompt'] = '{% trans %}Email address:{% endtrans %}'; + askbot['messages']['mergeQuestions'] = '{{ settings.WORDS_MERGE_QUESTIONS }}'; + askbot['messages']['enterDuplicateQuestionId'] = '{{ settings.WORDS_ENTER_DUPLICATE_QUESTION_ID }}'; {% if settings.READ_ONLY_MODE_ENABLED %} askbot['messages']['readOnlyMessage'] = "{{ settings.READ_ONLY_MESSAGE }}"; {% endif %} diff --git a/askbot/templates/question/answer_controls.html b/askbot/templates/question/answer_controls.html index 1c66f147..022287a1 100644 --- a/askbot/templates/question/answer_controls.html +++ b/askbot/templates/question/answer_controls.html @@ -32,6 +32,7 @@ <a class="question-delete" >{% if answer.deleted %}{% trans %}undelete{% endtrans %}{% else %}{% trans %}delete{% endtrans %}{% endif %}</a> </span> +{% if thread.is_moderated() %} <span id="post-{{answer.id}}-publish" class="action-link" @@ -48,6 +49,7 @@ >{% trans %}publish{% endtrans %}</a> {% endif %} </span> +{% endif %} <span class="action-link"> <a class="permant-link" href="{{ answer.get_absolute_url(question_post=question) }}" diff --git a/askbot/templates/question/question_controls.html b/askbot/templates/question/question_controls.html index ddb40e1d..e38157c2 100644 --- a/askbot/templates/question/question_controls.html +++ b/askbot/templates/question/question_controls.html @@ -30,6 +30,7 @@ {% else %} <a class="question-close" href="{% url close question.id %}">{% trans %}close{% endtrans %}</a> {% endif %} +<a class="question-merge">{% trans %}merge{% endtrans %}</a> <a id="post-{{question.id}}-delete" class="question-delete" diff --git a/askbot/tests/api_v1_tests.py b/askbot/tests/api_v1_tests.py index a90581cd..659ef9e1 100644 --- a/askbot/tests/api_v1_tests.py +++ b/askbot/tests/api_v1_tests.py @@ -39,7 +39,7 @@ class ApiV1Tests(AskbotTestCase): expected_keys = set([ 'id', 'view_count', 'title', 'answer_count', 'last_activity_by', 'last_activity_at', 'author', - 'url', 'tags', 'added_at', 'score' + 'url', 'tags', 'added_at', 'score', 'summary' ]) self.assertEqual(expected_keys, set(response_data['questions'][0].keys())) diff --git a/askbot/urls.py b/askbot/urls.py index 93fb879c..d06bff72 100644 --- a/askbot/urls.py +++ b/askbot/urls.py @@ -275,6 +275,11 @@ urlpatterns = patterns('', views.writers.answer, name='answer' ), + service_url( + r'^merge-questions/', + views.commands.merge_questions, + name='merge_questions' + ), service_url(#ajax only r'^vote$', views.commands.vote, diff --git a/askbot/views/api_v1.py b/askbot/views/api_v1.py index c8cab137..76062036 100644 --- a/askbot/views/api_v1.py +++ b/askbot/views/api_v1.py @@ -27,14 +27,16 @@ def get_user_data(user): def get_question_data(thread): """returns data dictionary for a given thread""" + question_post = thread._question_post() datum = { 'added_at': thread.added_at.strftime('%s'), - 'id': thread._question_post().id, + 'id': question_post.id, 'answer_count': thread.answer_count, 'view_count': thread.view_count, 'score': thread.score, 'last_activity_at': thread.last_activity_at.strftime('%s'), 'title': thread.title, + 'summary': question_post.summary, 'tags': thread.tagnames.strip().split(), 'url': site_url(thread.get_absolute_url()), } diff --git a/askbot/views/commands.py b/askbot/views/commands.py index d9866db9..86fa2abd 100644 --- a/askbot/views/commands.py +++ b/askbot/views/commands.py @@ -1566,3 +1566,20 @@ def publish_answer(request): #todo: notify enquirer by email about the post request.user.message_set.create(message=message) return {'redirect_url': answer.get_absolute_url()} + +@csrf.csrf_exempt +@decorators.ajax_only +@decorators.post_only +def merge_questions(request): + post_data = simplejson.loads(request.raw_post_data) + if request.user.is_anonymous(): + denied_msg = _('Sorry, only thread moderators can use this function') + raise exceptions.PermissionDenied(denied_msg) + + form_class = forms.GetDataForPostForm + from_form = form_class({'post_id': post_data['from_id']}) + to_form = form_class({'post_id': post_data['to_id']}) + if from_form.is_valid() and to_form.is_valid(): + from_question = get_object_or_404(models.Post, id=from_form.cleaned_data['post_id']) + to_question = get_object_or_404(models.Post, id=to_form.cleaned_data['post_id']) + request.user.merge_duplicate_questions(from_question, to_question) |