From 16a7963a9d7002730b97fd6cd7c418622befd609 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Wed, 23 May 2012 10:48:59 -0400 Subject: fixed bug with wrong reply address in the instant email notification --- askbot/models/__init__.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py index 0855abcd..7228ca61 100644 --- a/askbot/models/__init__.py +++ b/askbot/models/__init__.py @@ -2737,12 +2737,18 @@ def send_instant_notifications_about_activity_in_post( 'reply_action': 'post_comment' } if post.post_type in ('answer', 'comment'): - reply_addr = ReplyAddress.objects.create_new(**reply_args) + reply_addr = ReplyAddress.objects.create_new( + **reply_args + ).address elif post.post_type == 'question': - reply_with_comment_address = ReplyAddress.objects.create_new(**reply_args) + reply_with_comment_address = ReplyAddress.objects.create_new( + **reply_args + ).address #default action is to post answer reply_args['reply_action'] = 'post_answer' - reply_addr = ReplyAddress.objects.create_new(**reply_args) + reply_addr = ReplyAddress.objects.create_new( + **reply_args + ).address reply_to = 'reply-%s@%s' % ( reply_addr, -- cgit v1.2.3-1-g7c22 From 0941f7d6c01ec9a2f664d017480a321a8d9bf335 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Wed, 23 May 2012 11:08:05 -0400 Subject: fixed the Reply-To address for the comment reply for the question --- askbot/models/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py index 7228ca61..a87ab96d 100644 --- a/askbot/models/__init__.py +++ b/askbot/models/__init__.py @@ -2740,10 +2740,15 @@ def send_instant_notifications_about_activity_in_post( reply_addr = ReplyAddress.objects.create_new( **reply_args ).address + reply_to_with_comment = None elif post.post_type == 'question': reply_with_comment_address = ReplyAddress.objects.create_new( **reply_args ).address + reply_to_with_comment = 'reply-%s@%s' % ( + reply_addr, + askbot_settings.REPLY_BY_EMAIL_HOSTNAME + ) #default action is to post answer reply_args['reply_action'] = 'post_answer' reply_addr = ReplyAddress.objects.create_new( @@ -2762,7 +2767,7 @@ def send_instant_notifications_about_activity_in_post( to_user = user, from_user = update_activity.user, post = post, - reply_with_comment_address = reply_with_comment_address, + reply_with_comment_address = reply_to_with_comment, update_type = update_type, template = get_template('instant_notification.html') ) -- cgit v1.2.3-1-g7c22 From 753e82946529ce55351168483ff9f51a92b293af Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Thu, 24 May 2012 01:04:39 -0400 Subject: better handling of notification removal --- askbot/skins/common/media/js/utils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/askbot/skins/common/media/js/utils.js b/askbot/skins/common/media/js/utils.js index 8c2a478e..d5ce1c2d 100644 --- a/askbot/skins/common/media/js/utils.js +++ b/askbot/skins/common/media/js/utils.js @@ -109,7 +109,7 @@ var notify = function() { return { show: function(html) { if (html) { - $("body").css("margin-top", "2.2em"); + $("body").addClass('user-messages'); $(".notify span").html(html); } $(".notify").fadeIn("slow"); @@ -123,7 +123,7 @@ var notify = function() { ); } $(".notify").fadeOut("fast"); - $("body").css("margin-top", "0"); + $('body').removeClass('user-messages'); visible = false; }, isVisible: function() { return visible; } -- cgit v1.2.3-1-g7c22 From c839624b845c98c3531ba5b582bbce5b24519cf2 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Thu, 24 May 2012 14:54:53 -0400 Subject: fixed regex for the email reply separator and a bug in sending email alerts --- askbot/const/__init__.py | 2 +- askbot/models/__init__.py | 4 ++-- askbot/skins/default/media/style/style.less | 5 ++++- askbot/skins/default/templates/question.html | 1 + askbot/skins/default/templates/question/content.html | 2 +- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/askbot/const/__init__.py b/askbot/const/__init__.py index f3091800..ae1d7d2d 100644 --- a/askbot/const/__init__.py +++ b/askbot/const/__init__.py @@ -59,7 +59,7 @@ REPLY_WITH_COMMENT_TEMPLATE = _( 'Note: to reply with a comment, ' 'please use this link' ) -REPLY_SEPARATOR_REGEX = re.compile('==== .* -=-==', re.MULTILINE) +REPLY_SEPARATOR_REGEX = re.compile(r'==== .* -=-==', re.MULTILINE|re.DOTALL) ANSWER_SORT_METHODS = (#no translations needed here 'latest', 'oldest', 'votes' diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py index a87ab96d..90336e6c 100644 --- a/askbot/models/__init__.py +++ b/askbot/models/__init__.py @@ -2726,7 +2726,7 @@ def send_instant_notifications_about_activity_in_post( #of executive function with the activity log recording #TODO check user reputation headers = mail.thread_headers(post, origin_post, update_activity.activity_type) - reply_with_comment_address = None#only used for questions in some cases + reply_to_with_comment = None#only used for questions in some cases if askbot_settings.REPLY_BY_EMAIL: reply_addr = "noreply" if user.reputation >= askbot_settings.MIN_REP_TO_POST_BY_EMAIL: @@ -2746,7 +2746,7 @@ def send_instant_notifications_about_activity_in_post( **reply_args ).address reply_to_with_comment = 'reply-%s@%s' % ( - reply_addr, + reply_with_comment_address, askbot_settings.REPLY_BY_EMAIL_HOSTNAME ) #default action is to post answer diff --git a/askbot/skins/default/media/style/style.less b/askbot/skins/default/media/style/style.less index f015ae0c..1cc1be4b 100644 --- a/askbot/skins/default/media/style/style.less +++ b/askbot/skins/default/media/style/style.less @@ -1730,7 +1730,10 @@ ul#related-tags li { } #fmanswer_button{ - margin:8px 0px ; + margin:8px 0px; + } + #fmanswer_button.answer-own-question { + width: 150px; } .question-img-favorite:hover { background: url(../images/vote-favorite-on.png) diff --git a/askbot/skins/default/templates/question.html b/askbot/skins/default/templates/question.html index f22796db..bd48bd54 100644 --- a/askbot/skins/default/templates/question.html +++ b/askbot/skins/default/templates/question.html @@ -130,6 +130,7 @@ var add_answer_btn = document.getElementById('add-answer-btn'); if (askbot['data']['userIsAuthenticated']){ if (askbot['data']['userId'] == {{question.author_id}}){ + add_answer_btn.className += ' answer-own-question'; add_answer_btn.setAttribute( 'value', '{% trans %}Answer Your Own Question{% endtrans %}' diff --git a/askbot/skins/default/templates/question/content.html b/askbot/skins/default/templates/question/content.html index 738738dd..58b1bcca 100644 --- a/askbot/skins/default/templates/question/content.html +++ b/askbot/skins/default/templates/question/content.html @@ -37,5 +37,5 @@ {% include "question/new_answer_form.html" %} {# ==== END: question/new_answer_form.html ==== #} {% if request.user == question.author %}{# this is outside the form on purpose #} - + {%endif%} -- cgit v1.2.3-1-g7c22 From eee3ec8b7d78a04f4b3f8a56c7f73797318c9dd0 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Thu, 24 May 2012 15:09:51 -0400 Subject: added signature stripping to posting of responses by email --- askbot/models/__init__.py | 3 +++ askbot/models/reply_by_email.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py index 90336e6c..d7b838d5 100644 --- a/askbot/models/__init__.py +++ b/askbot/models/__init__.py @@ -207,6 +207,9 @@ def user_update_avatar_type(self): def user_strip_email_signature(self, text): """strips email signature from the end of the text""" + if self.email_signature == '': + return text + text = '\n'.join(text.splitlines())#normalize the line endings if text.endswith(self.email_signature): return text[0:-len(self.email_signature)] diff --git a/askbot/models/reply_by_email.py b/askbot/models/reply_by_email.py index 4398f693..786b38a5 100644 --- a/askbot/models/reply_by_email.py +++ b/askbot/models/reply_by_email.py @@ -79,6 +79,7 @@ class ReplyAddress(models.Model): to the same address""" assert self.was_used == True content, stored_files = mail.process_parts(parts) + content = self.user.strip_email_signature(content) self.user.edit_post( post = self.response_post, body_text = content, @@ -94,6 +95,7 @@ class ReplyAddress(models.Model): result = None #todo: delete stored files if this function fails content, stored_files = mail.process_parts(parts) + content = self.user.strip_email_signature(content) if self.post.post_type == 'answer': result = self.user.post_comment( -- cgit v1.2.3-1-g7c22 From 88ca6ed037d3d7f2c36fe38532b718e432487b46 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Thu, 24 May 2012 15:56:10 -0400 Subject: added signature extraction to processing of all responses --- askbot/lamson_handlers.py | 24 +++++------------------- askbot/models/__init__.py | 9 ++++++++- askbot/models/reply_by_email.py | 27 +++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 20 deletions(-) diff --git a/askbot/lamson_handlers.py b/askbot/lamson_handlers.py index d7dc7e13..3b6aefd7 100644 --- a/askbot/lamson_handlers.py +++ b/askbot/lamson_handlers.py @@ -221,27 +221,13 @@ def VALIDATE_EMAIL( todo: go a step further and """ content, stored_files = mail.process_parts(parts) + #save the signature and mark email as valid reply_code = reply_address_object.address + user = reply_address_object.user if reply_code in content: - - #extract the signature - tail = list() - for line in reversed(content.splitlines()): - #scan backwards from the end until the magic line - if reply_code in line: - break - tail.insert(0, line) - - #strip off the leading quoted lines, there could be one or two - #also strip empty lines - while tail[0].startswith('>') or tail[0].strip() == '': - tail.pop(0) - - signature = '\n'.join(tail) - - #save the signature and mark email as valid - user = reply_address_object.user - user.email_signature = signature + user.email_signature = reply_address_object.extract_user_signature( + content + ) user.email_isvalid = True user.save() diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py index d7b838d5..6e662b24 100644 --- a/askbot/models/__init__.py +++ b/askbot/models/__init__.py @@ -2578,6 +2578,7 @@ def format_instant_notification_email( from_user = None, post = None, reply_with_comment_address = None, + reply_code = None, update_type = None, template = None, ): @@ -2694,6 +2695,11 @@ def format_instant_notification_email( 'reply_separator': reply_separator } subject_line = _('"%(title)s"') % {'title': origin_post.thread.title} + + content = template.render(Context(update_data)) + if can_reply: + content += '

= askbot_settings.MIN_REP_TO_POST_BY_EMAIL: reply_args = { @@ -2771,6 +2777,7 @@ def send_instant_notifications_about_activity_in_post( from_user = update_activity.user, post = post, reply_with_comment_address = reply_to_with_comment, + reply_code = reply_addr, update_type = update_type, template = get_template('instant_notification.html') ) diff --git a/askbot/models/reply_by_email.py b/askbot/models/reply_by_email.py index 786b38a5..3940709d 100644 --- a/askbot/models/reply_by_email.py +++ b/askbot/models/reply_by_email.py @@ -74,6 +74,25 @@ class ReplyAddress(models.Model): """True if was used""" return self.used_at != None + def extract_user_signature(self, text): + if self.address in text: + #extract the signature + tail = list() + for line in reversed(text.splitlines()): + #scan backwards from the end until the magic line + if self.address in line: + break + tail.insert(0, line) + + #strip off the leading quoted lines, there could be one or two + #also strip empty lines + while tail[0].startswith('>') or tail[0].strip() == '': + tail.pop(0) + + return '\n'.join(tail) + else: + return '' + def edit_post(self, parts): """edits the created post upon repeated response to the same address""" @@ -95,6 +114,14 @@ class ReplyAddress(models.Model): result = None #todo: delete stored files if this function fails content, stored_files = mail.process_parts(parts) + + if self.address in content: + new_signature = self.extract_user_signature(content) + if new_signature != self.user.email_signature: + self.user.email_signature = new_signature + self.user.email_isvalid = True + self.user.save() + content = self.user.strip_email_signature(content) if self.post.post_type == 'answer': -- cgit v1.2.3-1-g7c22 From 12146c1a52b18c0385a67f69967643e60122f8e5 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Thu, 24 May 2012 23:17:44 -0400 Subject: bugfix in formatting answerable email notification --- askbot/models/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py index 6e662b24..17f25a03 100644 --- a/askbot/models/__init__.py +++ b/askbot/models/__init__.py @@ -2700,7 +2700,7 @@ def format_instant_notification_email( if can_reply: content += '

') or tail[0].strip() == '': - tail.pop(0) - - return '\n'.join(tail) - else: - return '' - - def edit_post(self, parts): + def edit_post(self, body_text, stored_files): """edits the created post upon repeated response to the same address""" assert self.was_used == True - content, stored_files = mail.process_parts(parts) - content = self.user.strip_email_signature(content) self.user.edit_post( post = self.response_post, - body_text = content, + body_text = stored_files, revision_comment = _('edited by email'), by_email = True ) self.response_post.thread.invalidate_cached_data() - def create_reply(self, parts): + def create_reply(self, body_text, stored_files): """creates a reply to the post which was emailed to the user """ result = None - #todo: delete stored files if this function fails - content, stored_files = mail.process_parts(parts) - - if self.address in content: - new_signature = self.extract_user_signature(content) - if new_signature != self.user.email_signature: - self.user.email_signature = new_signature - self.user.email_isvalid = True - self.user.save() - - content = self.user.strip_email_signature(content) - if self.post.post_type == 'answer': result = self.user.post_comment( self.post, - content, + body_text, by_email = True ) elif self.post.post_type == 'question': if self.reply_action == 'auto_answer_or_comment': - wordcount = len(content)/6#todo: this is a simplistic hack + wordcount = len(body_text)/6#todo: this is a simplistic hack if wordcount > askbot_settings.MIN_WORDS_FOR_ANSWER_BY_EMAIL: reply_action = 'post_answer' else: @@ -143,13 +110,13 @@ class ReplyAddress(models.Model): if reply_action == 'post_answer': result = self.user.post_answer( self.post, - content, + body_text, by_email = True ) elif reply_action == 'post_comment': result = self.user.post_comment( self.post, - content, + body_text, by_email = True ) else: @@ -160,7 +127,7 @@ class ReplyAddress(models.Model): elif self.post.post_type == 'comment': result = self.user.post_comment( self.post.parent, - content, + body_text, by_email = True ) result.thread.invalidate_cached_data() diff --git a/askbot/tests/reply_by_email_tests.py b/askbot/tests/reply_by_email_tests.py index e46d6b3d..b5132dcf 100644 --- a/askbot/tests/reply_by_email_tests.py +++ b/askbot/tests/reply_by_email_tests.py @@ -1,6 +1,6 @@ from django.utils.translation import ugettext as _ from askbot.models import ReplyAddress -from askbot.lamson_handlers import PROCESS, get_parts +from askbot.lamson_handlers import PROCESS, VALIDATE_EMAIL, get_parts from askbot import const @@ -8,13 +8,7 @@ from askbot.tests.utils import AskbotTestCase from askbot.models import Post, PostRevision TEST_CONTENT = 'Test content' -TEST_EMAIL_PARTS = ( - ('body', TEST_CONTENT), -) TEST_LONG_CONTENT = 'Test content' * 10 -TEST_LONG_EMAIL_PARTS = ( - ('body', TEST_LONG_CONTENT), -) class MockPart(object): def __init__(self, body): @@ -23,10 +17,23 @@ class MockPart(object): class MockMessage(dict): - def __init__(self, body, from_email): - self._body = body - self._part = MockPart(body) + def __init__( + self, content, from_email, signature = '', response_code = False + ): self.From= from_email + self['Subject'] = 'test subject' + + if response_code != False: + #in this case we modify the content + re_separator = const.REPLY_SEPARATOR_TEMPLATE % { + 'user_action': 'john did something', + 'instruction': 'reply above this line' + } + content += '\n\n\nToday someone wrote:\n' + re_separator + \ + '\nblah blah\n' + response_code + '\n' + signature + + self._body = content + self._part = MockPart(content) def body(self): return self._body @@ -35,7 +42,7 @@ class MockMessage(dict): """todo: add real file attachment""" return [self._part] -class EmailProcessingTests(AskbotTestCase): +class ReplyAddressModelTests(AskbotTestCase): def setUp(self): self.u1 = self.create_user(username='user1') @@ -79,30 +86,6 @@ class EmailProcessingTests(AskbotTestCase): self.assertEquals(self.answer.comments.count(), 2) self.assertEquals(self.answer.comments.all().order_by('-pk')[0].text.strip(), "This is a test reply") - - -class ReplyAddressModelTests(AskbotTestCase): - - def setUp(self): - self.u1 = self.create_user(username='user1') - self.u1.set_status('a') - self.u1.moderate_user_reputation(self.u1, reputation_change = 100, comment= "no comment") - self.u2 = self.create_user(username='user2') - self.u1.moderate_user_reputation(self.u2, reputation_change = 100, comment= "no comment") - self.u3 = self.create_user(username='user3') - self.u1.moderate_user_reputation(self.u3, reputation_change = 100, comment= "no comment") - - self.question = self.post_question( - user = self.u1, - follow = True, - ) - self.answer = self.post_answer( - user = self.u2, - question = self.question - ) - - self.comment = self.post_comment(user = self.u2, parent_post = self.answer) - def test_address_creation(self): self.assertEquals(ReplyAddress.objects.all().count(), 0) result = ReplyAddress.objects.create_new( @@ -118,7 +101,7 @@ class ReplyAddressModelTests(AskbotTestCase): post = self.answer, user = self.u1 ) - post = result.create_reply(TEST_EMAIL_PARTS) + post = result.create_reply(TEST_CONTENT, []) self.assertEquals(post.post_type, "comment") self.assertEquals(post.text, TEST_CONTENT) self.assertEquals(self.answer.comments.count(), 2) @@ -128,7 +111,7 @@ class ReplyAddressModelTests(AskbotTestCase): post = self.comment, user = self.u1 ) - post = result.create_reply(TEST_EMAIL_PARTS) + post = result.create_reply(TEST_CONTENT, []) self.assertEquals(post.post_type, "comment") self.assertEquals(post.text, TEST_CONTENT) self.assertEquals(self.answer.comments.count(), 2) @@ -139,7 +122,7 @@ class ReplyAddressModelTests(AskbotTestCase): post = self.question, user = self.u3 ) - post = result.create_reply(TEST_EMAIL_PARTS) + post = result.create_reply(TEST_CONTENT, []) self.assertEquals(post.post_type, "comment") self.assertEquals(post.text, TEST_CONTENT) @@ -148,6 +131,58 @@ class ReplyAddressModelTests(AskbotTestCase): post = self.question, user = self.u3 ) - post = result.create_reply(TEST_LONG_EMAIL_PARTS) + post = result.create_reply(TEST_LONG_CONTENT, []) self.assertEquals(post.post_type, "answer") self.assertEquals(post.text, TEST_LONG_CONTENT) + +class EmailSignatureDetectionTests(AskbotTestCase): + + def setUp(self): + self.u1 = self.create_user('user1', status = 'a') + self.u2 = self.create_user('user2', status = 'a') + + def test_detect_signature_in_response(self): + question = self.post_question(user = self.u1) + + #create a response address record + reply_token = ReplyAddress.objects.create_new( + post = question, + user = self.u2, + reply_action = 'post_answer' + ) + + self.u2.email_signature = '' + self.u2.save() + + msg = MockMessage( + 'some text', + self.u2.email, + signature = 'Yours Truly', + response_code = reply_token.address + ) + PROCESS(msg, address = reply_token.address) + + signature = self.reload_object(self.u2).email_signature + self.assertEqual(signature, 'Yours Truly') + + def test_detect_signature_in_welcome_response(self): + reply_token = ReplyAddress.objects.create_new( + user = self.u2, + reply_action = 'validate_email' + ) + self.u2.email_signature = '' + self.u2.save() + + msg = MockMessage( + 'some text', + self.u2.email, + signature = 'Yours Truly', + response_code = reply_token.address + ) + VALIDATE_EMAIL( + msg, + address = reply_token.address + ) + + signature = self.reload_object(self.u2).email_signature + self.assertEqual(signature, 'Yours Truly') diff --git a/askbot/utils/mail.py b/askbot/utils/mail.py index 4f653e6b..17eaf52c 100644 --- a/askbot/utils/mail.py +++ b/askbot/utils/mail.py @@ -250,7 +250,29 @@ def process_attachment(attachment): markdown_link = '!' + markdown_link return markdown_link, file_storage -def process_parts(parts): +def extract_user_signature(text, reply_code): + """extracts email signature as text trailing + the reply code""" + if reply_code in text: + #extract the signature + tail = list() + for line in reversed(text.splitlines()): + #scan backwards from the end until the magic line + if reply_code in line: + break + tail.insert(0, line) + + #strip off the leading quoted lines, there could be one or two + #also strip empty lines + while tail[0].startswith('>') or tail[0].strip() == '': + tail.pop(0) + + return '\n'.join(tail) + else: + return '' + + +def process_parts(parts, reply_code = None): """Process parts will upload the attachments and parse out the body, if body is multipart. Secondly - links to attachments will be added to the body of the question. @@ -274,10 +296,14 @@ def process_parts(parts): #if the response separator is present - #split the body with it, and discard the "so and so wrote:" part + if reply_code: + signature = extract_user_signature(body_markdown, reply_code) + else: + signature = None body_markdown = extract_reply(body_markdown) body_markdown += attachments_markdown - return body_markdown.strip(), stored_files + return body_markdown.strip(), stored_files, signature def process_emailed_question(from_address, subject, parts, tags = None): @@ -288,7 +314,7 @@ def process_emailed_question(from_address, subject, parts, tags = None): try: #todo: delete uploaded files when posting by email fails!!! - body, stored_files = process_parts(parts) + body, stored_files, unused = process_parts(parts) data = { 'sender': from_address, 'subject': subject, -- cgit v1.2.3-1-g7c22 From e466bd2a8c42bf3e1bf6a1cd7dddb8a747ce06ba Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Fri, 25 May 2012 13:24:59 -0400 Subject: fixed a bug in the notification sending --- askbot/models/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py index 17f25a03..1b254ee4 100644 --- a/askbot/models/__init__.py +++ b/askbot/models/__init__.py @@ -2698,7 +2698,7 @@ def format_instant_notification_email( content = template.render(Context(update_data)) if can_reply: - content += '

' return subject_line, content -- cgit v1.2.3-1-g7c22 From 591778e0dee017e6d066d075a0a6a6b7a911e385 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Fri, 25 May 2012 20:10:28 -0400 Subject: created module askbot.mail and moved lamson_handlers.py there --- askbot/doc/source/optional-modules.rst | 2 +- askbot/lamson_handlers.py | 278 -------------------------------- askbot/mail/__init__.py | 0 askbot/mail/lamson_handlers.py | 282 +++++++++++++++++++++++++++++++++ askbot/utils/mail.py | 7 +- 5 files changed, 287 insertions(+), 282 deletions(-) delete mode 100644 askbot/lamson_handlers.py create mode 100644 askbot/mail/__init__.py create mode 100644 askbot/mail/lamson_handlers.py diff --git a/askbot/doc/source/optional-modules.rst b/askbot/doc/source/optional-modules.rst index 0c013121..7489ef80 100644 --- a/askbot/doc/source/optional-modules.rst +++ b/askbot/doc/source/optional-modules.rst @@ -183,7 +183,7 @@ The minimum settings required to enable this feature are defining the port and b LAMSON_RECEIVER_CONFIG = {'host': 'your.ip.address', 'port': 25} - LAMSON_HANDLERS = ['askbot.lamson_handlers'] + LAMSON_HANDLERS = ['askbot.mail.lamson_handlers'] LAMSON_ROUTER_DEFAULTS = {'host': '.+'} diff --git a/askbot/lamson_handlers.py b/askbot/lamson_handlers.py deleted file mode 100644 index 03bdf43a..00000000 --- a/askbot/lamson_handlers.py +++ /dev/null @@ -1,278 +0,0 @@ -import re -import functools -from django.core.files.uploadedfile import SimpleUploadedFile -from django.conf import settings as django_settings -from django.template import Context -from django.utils.translation import ugettext as _ -from lamson.routing import route, stateless -from lamson.server import Relay -from askbot.models import ReplyAddress, Tag -from askbot.utils import mail -from askbot.conf import settings as askbot_settings -from askbot.skins.loaders import get_template - - -#we might end up needing to use something like this -#to distinguish the reply text from the quoted original message -""" -def _strip_message_qoute(message_text): - import re - result = message_text - pattern = "(?P" + \ - "On ([a-zA-Z0-9, :/<>@\.\"\[\]]* wrote:.*)|" + \ - "From: [\w@ \.]* \[mailto:[\w\.]*@[\w\.]*\].*|" + \ - "From: [\w@ \.]*(\n|\r\n)+Sent: [\*\w@ \.,:/]*(\n|\r\n)+To:.*(\n|\r\n)+.*|" + \ - "[- ]*Forwarded by [\w@ \.,:/]*.*|" + \ - "From: [\w@ \.<>\-]*(\n|\r\n)To: [\w@ \.<>\-]*(\n|\r\n)Date: [\w@ \.<>\-:,]*\n.*|" + \ - "From: [\w@ \.<>\-]*(\n|\r\n)To: [\w@ \.<>\-]*(\n|\r\n)Sent: [\*\w@ \.,:/]*(\n|\r\n).*|" + \ - "From: [\w@ \.<>\-]*(\n|\r\n)To: [\w@ \.<>\-]*(\n|\r\n)Subject:.*|" + \ - "(-| )*Original Message(-| )*.*)" - groups = re.search(pattern, email_text, re.IGNORECASE + re.DOTALL) - qoute = None - if not groups is None: - if groups.groupdict().has_key("qoute"): - qoute = groups.groupdict()["qoute"] - if qoute: - result = reslut.split(qoute)[0] - #if the last line contains an email message remove that one too - lines = result.splitlines(True) - if re.search(r'[\w\.]*@[\w\.]*\].*', lines[-1]): - result = '\n'.join(lines[:-1]) - return result -""" - -def get_disposition(part): - """return list of part's content dispositions - or an empty list - """ - dispositions = part.content_encoding.get('Content-Disposition', None) - if dispositions: - return dispositions[0] - else: - return list() - -def get_attachment_info(part): - return part.content_encoding['Content-Disposition'][1] - -def is_attachment(part): - """True if part content disposition is - attachment""" - return get_disposition(part) == 'attachment' - -def is_inline_attachment(part): - """True if part content disposition is - inline""" - return get_disposition(part) == 'inline' - -def format_attachment(part): - """takes message part and turns it into SimpleUploadedFile object""" - att_info = get_attachment_info(part) - name = att_info.get('filename', None) - content_type = get_content_type(part) - return SimpleUploadedFile(name, part.body, content_type) - -def get_content_type(part): - """return content type of the message part""" - return part.content_encoding.get('Content-Type', (None,))[0] - -def is_body(part): - """True, if part is plain text and is not attachment""" - if get_content_type(part) == 'text/plain': - if not is_attachment(part): - return True - return False - -def get_part_type(part): - if is_body(part): - return 'body' - elif is_attachment(part): - return 'attachment' - elif is_inline_attachment(part): - return 'inline' - -def get_parts(message): - """returns list of tuples (, ), - where is one of 'body', 'attachment', 'inline' - and - will be in the directly usable form: - * if it is 'body' - then it will be unicode text - * for attachment - it will be django's SimpleUploadedFile instance - - There may be multiple 'body' parts as well as others - usually the body is split when there are inline attachments present. - """ - - parts = list() - - simple_body = '' - if message.body(): - simple_body = message.body() - parts.append(('body', simple_body)) - - for part in message.walk(): - part_type = get_part_type(part) - if part_type == 'body': - part_content = part.body - if part_content == simple_body: - continue#avoid duplication - elif part_type in ('attachment', 'inline'): - part_content = format_attachment(part) - else: - continue - parts.append((part_type, part_content)) - return parts - -def process_reply(func): - @functools.wraps(func) - def wrapped(message, host = None, address = None): - """processes forwarding rules, and run the handler - in the case of error, send a bounce email - """ - try: - for rule in django_settings.LAMSON_FORWARD: - if re.match(rule['pattern'], message.base['to']): - relay = Relay(host=rule['host'], - port=rule['port'], debug=1) - relay.deliver(message) - return - except AttributeError: - pass - - error = None - try: - reply_address = ReplyAddress.objects.get( - address = address, - allowed_from_email = message.From - ) - - #here is the business part of this function - func( - from_address = message.From, - subject_line = message['Subject'], - parts = get_parts(message), - reply_address_object = reply_address - ) - - except ReplyAddress.DoesNotExist: - error = _("You were replying to an email address\ - unknown to the system or you were replying from a different address from the one where you\ - received the notification.") - except Exception, e: - import sys - sys.stderr.write(str(e)) - import traceback - sys.stderr.write(traceback.format_exc()) - - if error is not None: - template = get_template('email/reply_by_email_error.html') - body_text = template.render(Context({'error':error})) - mail.send_mail( - subject_line = "Error posting your reply", - body_text = body_text, - recipient_list = [message.From], - ) - - return wrapped - -@route('(addr)@(host)', addr = '.+') -@stateless -def ASK(message, host = None, addr = None): - """lamson handler for asking by email, - to the forum in general and to a specific group""" - - #we need to exclude some other emails by prefix - if addr.startswith('reply-'): - return - if addr.startswith('welcome-'): - return - - parts = get_parts(message) - from_address = message.From - subject = message['Subject']#why lamson does not give it normally? - if addr == 'ask': - mail.process_emailed_question(from_address, subject, parts) - else: - if askbot_settings.GROUP_EMAIL_ADDRESSES_ENABLED == False: - return - try: - group_tag = Tag.group_tags.get( - deleted = False, - name__iexact = addr - ) - mail.process_emailed_question( - from_address, subject, parts, tags = [group_tag.name, ] - ) - except Tag.DoesNotExist: - #do nothing because this handler will match all emails - return - except Tag.MultipleObjectsReturned: - return - -@route('welcome-(address)@(host)', address='.+') -@stateless -@process_reply -def VALIDATE_EMAIL( - parts = None, - reply_address_object = None, - from_address = None, - **kwargs -): - """process the validation email and save - the email signature - todo: go a step further and - """ - reply_code = reply_address_object.address - try: - content, stored_files, signature = mail.process_parts(parts, reply_code) - user = reply_address_object.user - if signature and signature != user.email_signature: - user.email_signature = signature - user.email_isvalid = True - user.save() - - data = { - 'site_name': askbot_settings.APP_SHORT_NAME, - 'site_url': askbot_settings.APP_URL, - 'ask_address': 'ask@' + askbot_settings.REPLY_BY_EMAIL_HOSTNAME - } - template = get_template('email/re_welcome_lamson_on.html') - - mail.send_mail( - subject_line = _('Re: Welcome to %(site_name)s') % data, - body_text = template.render(Context(data)), - recipient_list = [from_address,] - ) - except ValueError: - raise ValueError( - _( - 'Please reply to the welcome email ' - 'without editing it' - ) - ) - -@route('reply-(address)@(host)', address='.+') -@stateless -@process_reply -def PROCESS( - parts = None, - reply_address_object = None, - **kwargs -): - """handler to process the emailed message - and make a post to askbot based on the contents of - the email, including the text body and the file attachments""" - #split email into bits - reply_code = reply_address_object.address - body_text, stored_files, signature = mail.process_parts(parts, reply_code) - - #update signature and validate email address - user = reply_address_object.user - if signature and signature != user.email_signature: - user.email_signature = signature - user.email_isvalid = True - user.save()#todo: actually, saving is not necessary, if nothing changed - - if reply_address_object.was_used: - action = reply_address_object.edit_post - else: - action = reply_address_object.create_reply - action(body_text, stored_files) diff --git a/askbot/mail/__init__.py b/askbot/mail/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/askbot/mail/lamson_handlers.py b/askbot/mail/lamson_handlers.py new file mode 100644 index 00000000..f3760e9d --- /dev/null +++ b/askbot/mail/lamson_handlers.py @@ -0,0 +1,282 @@ +import re +import functools +from django.core.files.uploadedfile import SimpleUploadedFile +from django.conf import settings as django_settings +from django.template import Context +from django.utils.translation import ugettext as _ +from lamson.routing import route, stateless +from lamson.server import Relay +from askbot.models import ReplyAddress, Tag +from askbot.utils import mail +from askbot.conf import settings as askbot_settings +from askbot.skins.loaders import get_template + + +#we might end up needing to use something like this +#to distinguish the reply text from the quoted original message +""" +def _strip_message_qoute(message_text): + import re + result = message_text + pattern = "(?P" + \ + "On ([a-zA-Z0-9, :/<>@\.\"\[\]]* wrote:.*)|" + \ + "From: [\w@ \.]* \[mailto:[\w\.]*@[\w\.]*\].*|" + \ + "From: [\w@ \.]*(\n|\r\n)+Sent: [\*\w@ \.,:/]*(\n|\r\n)+To:.*(\n|\r\n)+.*|" + \ + "[- ]*Forwarded by [\w@ \.,:/]*.*|" + \ + "From: [\w@ \.<>\-]*(\n|\r\n)To: [\w@ \.<>\-]*(\n|\r\n)Date: [\w@ \.<>\-:,]*\n.*|" + \ + "From: [\w@ \.<>\-]*(\n|\r\n)To: [\w@ \.<>\-]*(\n|\r\n)Sent: [\*\w@ \.,:/]*(\n|\r\n).*|" + \ + "From: [\w@ \.<>\-]*(\n|\r\n)To: [\w@ \.<>\-]*(\n|\r\n)Subject:.*|" + \ + "(-| )*Original Message(-| )*.*)" + groups = re.search(pattern, email_text, re.IGNORECASE + re.DOTALL) + qoute = None + if not groups is None: + if groups.groupdict().has_key("qoute"): + qoute = groups.groupdict()["qoute"] + if qoute: + result = reslut.split(qoute)[0] + #if the last line contains an email message remove that one too + lines = result.splitlines(True) + if re.search(r'[\w\.]*@[\w\.]*\].*', lines[-1]): + result = '\n'.join(lines[:-1]) + return result +""" + +def get_disposition(part): + """return list of part's content dispositions + or an empty list + """ + dispositions = part.content_encoding.get('Content-Disposition', None) + if dispositions: + return dispositions[0] + else: + return list() + +def get_attachment_info(part): + return part.content_encoding['Content-Disposition'][1] + +def is_attachment(part): + """True if part content disposition is + attachment""" + return get_disposition(part) == 'attachment' + +def is_inline_attachment(part): + """True if part content disposition is + inline""" + return get_disposition(part) == 'inline' + +def format_attachment(part): + """takes message part and turns it into SimpleUploadedFile object""" + att_info = get_attachment_info(part) + name = att_info.get('filename', None) + content_type = get_content_type(part) + return SimpleUploadedFile(name, part.body, content_type) + +def get_content_type(part): + """return content type of the message part""" + return part.content_encoding.get('Content-Type', (None,))[0] + +def is_body(part): + """True, if part is plain text and is not attachment""" + if get_content_type(part) == 'text/plain': + if not is_attachment(part): + return True + return False + +def get_part_type(part): + if is_body(part): + return 'body' + elif is_attachment(part): + return 'attachment' + elif is_inline_attachment(part): + return 'inline' + +def get_parts(message): + """returns list of tuples (, ), + where is one of 'body', 'attachment', 'inline' + and - will be in the directly usable form: + * if it is 'body' - then it will be unicode text + * for attachment - it will be django's SimpleUploadedFile instance + + There may be multiple 'body' parts as well as others + usually the body is split when there are inline attachments present. + """ + + parts = list() + + simple_body = '' + if message.body(): + simple_body = message.body() + parts.append(('body', simple_body)) + + for part in message.walk(): + part_type = get_part_type(part) + if part_type == 'body': + part_content = part.body + if part_content == simple_body: + continue#avoid duplication + elif part_type in ('attachment', 'inline'): + part_content = format_attachment(part) + else: + continue + parts.append((part_type, part_content)) + return parts + +def process_reply(func): + @functools.wraps(func) + def wrapped(message, host = None, address = None): + """processes forwarding rules, and run the handler + in the case of error, send a bounce email + """ + try: + for rule in django_settings.LAMSON_FORWARD: + if re.match(rule['pattern'], message.base['to']): + relay = Relay(host=rule['host'], + port=rule['port'], debug=1) + relay.deliver(message) + return + except AttributeError: + pass + + error = None + try: + reply_address = ReplyAddress.objects.get( + address = address, + allowed_from_email = message.From + ) + + #here is the business part of this function + func( + from_address = message.From, + subject_line = message['Subject'], + parts = get_parts(message), + reply_address_object = reply_address + ) + + except ReplyAddress.DoesNotExist: + error = _("You were replying to an email address\ + unknown to the system or you were replying from a different address from the one where you\ + received the notification.") + except Exception, e: + import sys + sys.stderr.write(str(e)) + import traceback + sys.stderr.write(traceback.format_exc()) + + if error is not None: + template = get_template('email/reply_by_email_error.html') + body_text = template.render(Context({'error':error})) + mail.send_mail( + subject_line = "Error posting your reply", + body_text = body_text, + recipient_list = [message.From], + ) + + return wrapped + +@route('(addr)@(host)', addr = '.+') +@stateless +def ASK(message, host = None, addr = None): + """lamson handler for asking by email, + to the forum in general and to a specific group""" + + #we need to exclude some other emails by prefix + if addr.startswith('reply-'): + return + if addr.startswith('welcome-'): + return + + parts = get_parts(message) + from_address = message.From + subject = message['Subject']#why lamson does not give it normally? + body_text, stored_files, unused = process_parts(parts) + if addr == 'ask': + mail.process_emailed_question( + from_address, subject, body_text, stored_files + ) + else: + if askbot_settings.GROUP_EMAIL_ADDRESSES_ENABLED == False: + return + try: + group_tag = Tag.group_tags.get( + deleted = False, + name__iexact = addr + ) + mail.process_emailed_question( + from_address, subject, body_text, stored_files, + tags = [group_tag.name, ] + ) + except Tag.DoesNotExist: + #do nothing because this handler will match all emails + return + except Tag.MultipleObjectsReturned: + return + +@route('welcome-(address)@(host)', address='.+') +@stateless +@process_reply +def VALIDATE_EMAIL( + parts = None, + reply_address_object = None, + from_address = None, + **kwargs +): + """process the validation email and save + the email signature + todo: go a step further and + """ + reply_code = reply_address_object.address + try: + content, stored_files, signature = mail.process_parts(parts, reply_code) + user = reply_address_object.user + if signature and signature != user.email_signature: + user.email_signature = signature + user.email_isvalid = True + user.save() + + data = { + 'site_name': askbot_settings.APP_SHORT_NAME, + 'site_url': askbot_settings.APP_URL, + 'ask_address': 'ask@' + askbot_settings.REPLY_BY_EMAIL_HOSTNAME + } + template = get_template('email/re_welcome_lamson_on.html') + + mail.send_mail( + subject_line = _('Re: Welcome to %(site_name)s') % data, + body_text = template.render(Context(data)), + recipient_list = [from_address,] + ) + except ValueError: + raise ValueError( + _( + 'Please reply to the welcome email ' + 'without editing it' + ) + ) + +@route('reply-(address)@(host)', address='.+') +@stateless +@process_reply +def PROCESS( + parts = None, + reply_address_object = None, + **kwargs +): + """handler to process the emailed message + and make a post to askbot based on the contents of + the email, including the text body and the file attachments""" + #split email into bits + reply_code = reply_address_object.address + body_text, stored_files, signature = mail.process_parts(parts, reply_code) + + #update signature and validate email address + user = reply_address_object.user + if signature and signature != user.email_signature: + user.email_signature = signature + user.email_isvalid = True + user.save()#todo: actually, saving is not necessary, if nothing changed + + if reply_address_object.was_used: + action = reply_address_object.edit_post + else: + action = reply_address_object.create_reply + action(body_text, stored_files) diff --git a/askbot/utils/mail.py b/askbot/utils/mail.py index 17eaf52c..35f3ac62 100644 --- a/askbot/utils/mail.py +++ b/askbot/utils/mail.py @@ -306,7 +306,9 @@ def process_parts(parts, reply_code = None): return body_markdown.strip(), stored_files, signature -def process_emailed_question(from_address, subject, parts, tags = None): +def process_emailed_question( + from_address, subject, body_text, stored_files, tags = None +): """posts question received by email or bounces the message""" #a bunch of imports here, to avoid potential circular import issues from askbot.forms import AskByEmailForm @@ -314,11 +316,10 @@ def process_emailed_question(from_address, subject, parts, tags = None): try: #todo: delete uploaded files when posting by email fails!!! - body, stored_files, unused = process_parts(parts) data = { 'sender': from_address, 'subject': subject, - 'body_text': body + 'body_text': body_text } form = AskByEmailForm(data) if form.is_valid(): -- cgit v1.2.3-1-g7c22 From c5eeea2e142d43ff953d4aba5a40110e6e13aa47 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Fri, 25 May 2012 20:23:17 -0400 Subject: moved module askbot.utils.mail -> askbot.mail --- askbot/forms.py | 2 +- askbot/mail/__init__.py | 374 +++++++++++++++++++++ askbot/mail/lamson_handlers.py | 2 +- .../management/commands/post_emailed_questions.py | 2 +- .../commands/send_accept_answer_reminders.py | 2 +- askbot/management/commands/send_email.py | 2 +- askbot/management/commands/send_email_alerts.py | 2 +- .../commands/send_unanswered_question_reminders.py | 2 +- askbot/models/__init__.py | 2 +- askbot/models/post.py | 2 +- askbot/models/reply_by_email.py | 2 +- askbot/tests/email_alert_tests.py | 2 +- askbot/utils/mail.py | 374 --------------------- askbot/views/commands.py | 2 +- askbot/views/meta.py | 2 +- askbot/views/users.py | 2 +- 16 files changed, 388 insertions(+), 388 deletions(-) delete mode 100644 askbot/utils/mail.py diff --git a/askbot/forms.py b/askbot/forms.py index 252c002e..cced18e9 100644 --- a/askbot/forms.py +++ b/askbot/forms.py @@ -8,7 +8,7 @@ from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType from django_countries import countries from askbot.utils.forms import NextUrlField, UserNameField -from askbot.utils.mail import extract_first_email_address +from askbot.mail import extract_first_email_address from recaptcha_works.fields import RecaptchaField from askbot.conf import settings as askbot_settings import logging diff --git a/askbot/mail/__init__.py b/askbot/mail/__init__.py index e69de29b..35f3ac62 100644 --- a/askbot/mail/__init__.py +++ b/askbot/mail/__init__.py @@ -0,0 +1,374 @@ +"""functions that send email in askbot +these automatically catch email-related exceptions +""" +import os +import smtplib +import logging +from django.core import mail +from django.conf import settings as django_settings +from django.core.exceptions import PermissionDenied +from django.forms import ValidationError +from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import string_concat +from askbot import exceptions +from askbot import const +from askbot.conf import settings as askbot_settings +from askbot.utils import url_utils +from askbot.utils.file_utils import store_file +#todo: maybe send_mail functions belong to models +#or the future API +def prefix_the_subject_line(subject): + """prefixes the subject line with the + EMAIL_SUBJECT_LINE_PREFIX either from + from live settings, which take default from django + """ + prefix = askbot_settings.EMAIL_SUBJECT_PREFIX + if prefix != '': + subject = prefix.strip() + ' ' + subject.strip() + return subject + +def extract_first_email_address(text): + """extract first matching email address + from text string + returns ``None`` if there are no matches + """ + match = const.EMAIL_REGEX.search(text) + if match: + return match.group(0) + else: + return None + +def thread_headers(post, orig_post, update): + """modify headers for email messages, so + that emails appear as threaded conversations in gmail""" + suffix_id = django_settings.SERVER_EMAIL + if update == const.TYPE_ACTIVITY_ASK_QUESTION: + msg_id = "NQ-%s-%s" % (post.id, suffix_id) + headers = {'Message-ID': msg_id} + elif update == const.TYPE_ACTIVITY_ANSWER: + msg_id = "NA-%s-%s" % (post.id, suffix_id) + orig_id = "NQ-%s-%s" % (orig_post.id, suffix_id) + headers = {'Message-ID': msg_id, + 'In-Reply-To': orig_id} + elif update == const.TYPE_ACTIVITY_UPDATE_QUESTION: + msg_id = "UQ-%s-%s-%s" % (post.id, post.last_edited_at, suffix_id) + orig_id = "NQ-%s-%s" % (orig_post.id, suffix_id) + headers = {'Message-ID': msg_id, + 'In-Reply-To': orig_id} + elif update == const.TYPE_ACTIVITY_COMMENT_QUESTION: + msg_id = "CQ-%s-%s" % (post.id, suffix_id) + orig_id = "NQ-%s-%s" % (orig_post.id, suffix_id) + headers = {'Message-ID': msg_id, + 'In-Reply-To': orig_id} + elif update == const.TYPE_ACTIVITY_UPDATE_ANSWER: + msg_id = "UA-%s-%s-%s" % (post.id, post.last_edited_at, suffix_id) + orig_id = "NQ-%s-%s" % (orig_post.id, suffix_id) + headers = {'Message-ID': msg_id, + 'In-Reply-To': orig_id} + elif update == const.TYPE_ACTIVITY_COMMENT_ANSWER: + msg_id = "CA-%s-%s" % (post.id, suffix_id) + orig_id = "NQ-%s-%s" % (orig_post.id, suffix_id) + headers = {'Message-ID': msg_id, + 'In-Reply-To': orig_id} + else: + # Unknown type -> Can't set headers + return {} + + return headers + +def send_mail( + subject_line = None, + body_text = None, + from_email = django_settings.DEFAULT_FROM_EMAIL, + recipient_list = None, + activity_type = None, + related_object = None, + headers = None, + raise_on_failure = False, + ): + """ + todo: remove parameters not relevant to the function + sends email message + logs email sending activity + and any errors are reported as critical + in the main log file + + related_object is not mandatory, other arguments + are. related_object (if given, will be saved in + the activity record) + + if raise_on_failure is True, exceptions.EmailNotSent is raised + """ + try: + assert(subject_line is not None) + subject_line = prefix_the_subject_line(subject_line) + msg = mail.EmailMessage( + subject_line, + body_text, + from_email, + recipient_list, + headers = headers + ) + msg.content_subtype = 'html' + msg.send() + if related_object is not None: + assert(activity_type is not None) + except Exception, error: + logging.critical(unicode(error)) + if raise_on_failure == True: + raise exceptions.EmailNotSent(unicode(error)) + +def mail_moderators( + subject_line = '', + body_text = '', + raise_on_failure = False): + """sends email to forum moderators and admins + """ + from django.db.models import Q + from askbot.models import User + recipient_list = User.objects.filter( + Q(status='m') | Q(is_superuser=True) + ).filter( + is_active = True + ).values_list('email', flat=True) + recipient_list = set(recipient_list) + + from_email = '' + if hasattr(django_settings, 'DEFAULT_FROM_EMAIL'): + from_email = django_settings.DEFAULT_FROM_EMAIL + + try: + mail.send_mail(subject_line, body_text, from_email, recipient_list) + except smtplib.SMTPException, error: + logging.critical(unicode(error)) + if raise_on_failure == True: + raise exceptions.EmailNotSent(unicode(error)) + +INSTRUCTIONS_PREAMBLE = _('

To ask by email, please:

') +QUESTION_TITLE_INSTRUCTION = _( + '
  • Type title in the subject line
  • ' +) +QUESTION_DETAILS_INSTRUCTION = _( + '
  • Type details of your question into the email body
  • ' +) +OPTIONAL_TAGS_INSTRUCTION = _( +"""
  • The beginning of the subject line can contain tags, +enclosed in the square brackets like so: [Tag1; Tag2]
  • """ +) +REQUIRED_TAGS_INSTRUCTION = _( +"""
  • In the beginning of the subject add at least one tag +enclosed in the brackets like so: [Tag1; Tag2].
  • """ +) +TAGS_INSTRUCTION_FOOTNOTE = _( +"""

    Note that a tag may consist of more than one word, to separate +the tags, use a semicolon or a comma, for example, [One tag; Other tag]

    """ +) + +def bounce_email(email, subject, reason = None, body_text = None): + """sends a bounce email at address ``email``, with the subject + line ``subject``, accepts several reasons for the bounce: + * ``'problem_posting'``, ``unknown_user`` and ``permission_denied`` + * ``body_text`` in an optional parameter that allows to append + extra text to the message + """ + if reason == 'problem_posting': + error_message = _( + '

    Sorry, there was an error posting your question ' + 'please contact the %(site)s administrator

    ' + ) % {'site': askbot_settings.APP_SHORT_NAME} + + if askbot_settings.TAGS_ARE_REQUIRED: + error_message = string_concat( + INSTRUCTIONS_PREAMBLE, + '
      ', + QUESTION_TITLE_INSTRUCTION, + REQUIRED_TAGS_INSTRUCTION, + QUESTION_DETAILS_INSTRUCTION, + '
    ', + TAGS_INSTRUCTION_FOOTNOTE + ) + else: + error_message = string_concat( + INSTRUCTIONS_PREAMBLE, + '
      ', + QUESTION_TITLE_INSTRUCTION, + QUESTION_DETAILS_INSTRUCTION, + OPTIONAL_TAGS_INSTRUCTION, + '
    ', + TAGS_INSTRUCTION_FOOTNOTE + ) + + elif reason == 'unknown_user': + error_message = _( + '

    Sorry, in order to post questions on %(site)s ' + 'by email, please register first

    ' + ) % { + 'site': askbot_settings.APP_SHORT_NAME, + 'url': url_utils.get_login_url() + } + elif reason == 'permission_denied': + error_message = _( + '

    Sorry, your question could not be posted ' + 'due to insufficient privileges of your user account

    ' + ) + else: + raise ValueError('unknown reason to bounce an email: "%s"' % reason) + + if body_text != None: + error_message = string_concat(error_message, body_text) + + #print 'sending email' + #print email + #print subject + #print error_message + send_mail( + recipient_list = (email,), + subject_line = 'Re: ' + subject, + body_text = error_message + ) + +def extract_reply(text): + """take the part above the separator + and discard the last line above the separator""" + if const.REPLY_SEPARATOR_REGEX.search(text): + text = const.REPLY_SEPARATOR_REGEX.split(text)[0] + return '\n'.join(text.splitlines(True)[:-3]) + else: + return text + +def process_attachment(attachment): + """will save a single + attachment and return + link to file in the markdown format and the + file storage object + """ + file_storage, file_name, file_url = store_file(attachment) + markdown_link = '[%s](%s) ' % (attachment.name, file_url) + file_extension = os.path.splitext(attachment.name)[1] + #todo: this is a hack - use content type + if file_extension.lower() in ('.png', '.jpg', '.jpeg', '.gif'): + markdown_link = '!' + markdown_link + return markdown_link, file_storage + +def extract_user_signature(text, reply_code): + """extracts email signature as text trailing + the reply code""" + if reply_code in text: + #extract the signature + tail = list() + for line in reversed(text.splitlines()): + #scan backwards from the end until the magic line + if reply_code in line: + break + tail.insert(0, line) + + #strip off the leading quoted lines, there could be one or two + #also strip empty lines + while tail[0].startswith('>') or tail[0].strip() == '': + tail.pop(0) + + return '\n'.join(tail) + else: + return '' + + +def process_parts(parts, reply_code = None): + """Process parts will upload the attachments and parse out the + body, if body is multipart. Secondly - links to attachments + will be added to the body of the question. + Returns ready to post body of the message and the list + of uploaded files. + """ + body_markdown = '' + stored_files = list() + attachments_markdown = '' + for (part_type, content) in parts: + if part_type == 'attachment': + markdown, stored_file = process_attachment(content) + stored_files.append(stored_file) + attachments_markdown += '\n\n' + markdown + elif part_type == 'body': + body_markdown += '\n\n' + content + elif part_type == 'inline': + markdown, stored_file = process_attachment(content) + stored_files.append(stored_file) + body_markdown += markdown + + #if the response separator is present - + #split the body with it, and discard the "so and so wrote:" part + if reply_code: + signature = extract_user_signature(body_markdown, reply_code) + else: + signature = None + body_markdown = extract_reply(body_markdown) + + body_markdown += attachments_markdown + return body_markdown.strip(), stored_files, signature + + +def process_emailed_question( + from_address, subject, body_text, stored_files, tags = None +): + """posts question received by email or bounces the message""" + #a bunch of imports here, to avoid potential circular import issues + from askbot.forms import AskByEmailForm + from askbot.models import User + + try: + #todo: delete uploaded files when posting by email fails!!! + data = { + 'sender': from_address, + 'subject': subject, + 'body_text': body_text + } + form = AskByEmailForm(data) + if form.is_valid(): + email_address = form.cleaned_data['email'] + user = User.objects.get( + email__iexact = email_address + ) + + if user.email_isvalid == False: + raise PermissionDenied('Lacking email signature') + + tagnames = form.cleaned_data['tagnames'] + title = form.cleaned_data['title'] + body_text = form.cleaned_data['body_text'] + + #defect - here we might get "too many tags" issue + if tags: + tagnames += ' ' + ' '.join(tags) + + stripped_body_text = user.strip_email_signature(body_text) + if stripped_body_text == body_text and user.email_signature: + #todo: send an email asking to update the signature + raise ValueError('email signature changed') + + user.post_question( + title = title, + tags = tagnames.strip(), + body_text = stripped_body_text, + by_email = True, + email_address = from_address + ) + else: + raise ValidationError() + + except User.DoesNotExist: + bounce_email(email_address, subject, reason = 'unknown_user') + except User.MultipleObjectsReturned: + bounce_email(email_address, subject, reason = 'problem_posting') + except PermissionDenied, error: + bounce_email( + email_address, + subject, + reason = 'permission_denied', + body_text = unicode(error) + ) + except ValidationError: + if from_address: + bounce_email( + from_address, + subject, + reason = 'problem_posting', + ) diff --git a/askbot/mail/lamson_handlers.py b/askbot/mail/lamson_handlers.py index f3760e9d..a802fbb6 100644 --- a/askbot/mail/lamson_handlers.py +++ b/askbot/mail/lamson_handlers.py @@ -7,7 +7,7 @@ from django.utils.translation import ugettext as _ from lamson.routing import route, stateless from lamson.server import Relay from askbot.models import ReplyAddress, Tag -from askbot.utils import mail +from askbot import mail from askbot.conf import settings as askbot_settings from askbot.skins.loaders import get_template diff --git a/askbot/management/commands/post_emailed_questions.py b/askbot/management/commands/post_emailed_questions.py index cc77b57f..acddcf43 100644 --- a/askbot/management/commands/post_emailed_questions.py +++ b/askbot/management/commands/post_emailed_questions.py @@ -22,7 +22,7 @@ import base64 from django.conf import settings as django_settings from django.core.management.base import NoArgsCommand, CommandError from askbot.conf import settings as askbot_settings -from askbot.utils import mail +from askbot import mail class CannotParseEmail(Exception): """This exception will bounce the email""" diff --git a/askbot/management/commands/send_accept_answer_reminders.py b/askbot/management/commands/send_accept_answer_reminders.py index 47358a99..3a20ba27 100644 --- a/askbot/management/commands/send_accept_answer_reminders.py +++ b/askbot/management/commands/send_accept_answer_reminders.py @@ -6,7 +6,7 @@ from askbot import const from askbot.conf import settings as askbot_settings from django.utils.translation import ugettext as _ from django.utils.translation import ungettext -from askbot.utils import mail +from askbot import mail from askbot.utils.classes import ReminderSchedule DEBUG_THIS_COMMAND = False diff --git a/askbot/management/commands/send_email.py b/askbot/management/commands/send_email.py index 2493c51b..ff36b19f 100644 --- a/askbot/management/commands/send_email.py +++ b/askbot/management/commands/send_email.py @@ -1,4 +1,4 @@ -from askbot.utils.mail import send_mail +from askbot.mail import send_mail from django.core.exceptions import ValidationError from django.core.management.base import BaseCommand, CommandError from django.core.validators import validate_email diff --git a/askbot/management/commands/send_email_alerts.py b/askbot/management/commands/send_email_alerts.py index b7624e21..e890452d 100644 --- a/askbot/management/commands/send_email_alerts.py +++ b/askbot/management/commands/send_email_alerts.py @@ -12,7 +12,7 @@ from askbot.conf import settings as askbot_settings from django.utils.datastructures import SortedDict from django.contrib.contenttypes.models import ContentType from askbot import const -from askbot.utils import mail +from askbot import mail from askbot.utils.slug import slugify DEBUG_THIS_COMMAND = False diff --git a/askbot/management/commands/send_unanswered_question_reminders.py b/askbot/management/commands/send_unanswered_question_reminders.py index 424e45cc..82b6ecd8 100644 --- a/askbot/management/commands/send_unanswered_question_reminders.py +++ b/askbot/management/commands/send_unanswered_question_reminders.py @@ -3,7 +3,7 @@ from askbot import models from askbot import const from askbot.conf import settings as askbot_settings from django.utils.translation import ungettext -from askbot.utils import mail +from askbot import mail from askbot.utils.classes import ReminderSchedule from askbot.models.question import Thread diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py index 1b254ee4..cba48249 100644 --- a/askbot/models/__init__.py +++ b/askbot/models/__init__.py @@ -40,7 +40,7 @@ from askbot.utils.decorators import auto_now_timestamp from askbot.utils.slug import slugify from askbot.utils.diff import textDiff as htmldiff from askbot.utils.url_utils import strip_path -from askbot.utils import mail +from askbot import mail def get_model(model_name): """a shortcut for getting model for an askbot app""" diff --git a/askbot/models/post.py b/askbot/models/post.py index b6ee6d2a..9d1c9513 100644 --- a/askbot/models/post.py +++ b/askbot/models/post.py @@ -1784,7 +1784,7 @@ class PostRevision(models.Model): self.post.thread.save() #above changes will hide post from the public display if self.by_email: - from askbot.utils.mail import send_mail + from askbot.mail import send_mail email_context = { 'site': askbot_settings.APP_SHORT_NAME } diff --git a/askbot/models/reply_by_email.py b/askbot/models/reply_by_email.py index 058fbbcc..26111901 100644 --- a/askbot/models/reply_by_email.py +++ b/askbot/models/reply_by_email.py @@ -8,7 +8,7 @@ from django.utils.translation import ugettext_lazy as _ from askbot.models.post import Post from askbot.models.base import BaseQuerySetManager from askbot.conf import settings as askbot_settings -from askbot.utils import mail +from askbot import mail class ReplyAddressManager(BaseQuerySetManager): """A manager for the :class:`ReplyAddress` model""" diff --git a/askbot/tests/email_alert_tests.py b/askbot/tests/email_alert_tests.py index abdb1c58..828b341b 100644 --- a/askbot/tests/email_alert_tests.py +++ b/askbot/tests/email_alert_tests.py @@ -11,7 +11,7 @@ from django.test import TestCase from django.test.client import Client from askbot.tests import utils from askbot import models -from askbot.utils import mail +from askbot import mail from askbot.conf import settings as askbot_settings from askbot import const from askbot.models.question import Thread diff --git a/askbot/utils/mail.py b/askbot/utils/mail.py deleted file mode 100644 index 35f3ac62..00000000 --- a/askbot/utils/mail.py +++ /dev/null @@ -1,374 +0,0 @@ -"""functions that send email in askbot -these automatically catch email-related exceptions -""" -import os -import smtplib -import logging -from django.core import mail -from django.conf import settings as django_settings -from django.core.exceptions import PermissionDenied -from django.forms import ValidationError -from django.utils.translation import ugettext_lazy as _ -from django.utils.translation import string_concat -from askbot import exceptions -from askbot import const -from askbot.conf import settings as askbot_settings -from askbot.utils import url_utils -from askbot.utils.file_utils import store_file -#todo: maybe send_mail functions belong to models -#or the future API -def prefix_the_subject_line(subject): - """prefixes the subject line with the - EMAIL_SUBJECT_LINE_PREFIX either from - from live settings, which take default from django - """ - prefix = askbot_settings.EMAIL_SUBJECT_PREFIX - if prefix != '': - subject = prefix.strip() + ' ' + subject.strip() - return subject - -def extract_first_email_address(text): - """extract first matching email address - from text string - returns ``None`` if there are no matches - """ - match = const.EMAIL_REGEX.search(text) - if match: - return match.group(0) - else: - return None - -def thread_headers(post, orig_post, update): - """modify headers for email messages, so - that emails appear as threaded conversations in gmail""" - suffix_id = django_settings.SERVER_EMAIL - if update == const.TYPE_ACTIVITY_ASK_QUESTION: - msg_id = "NQ-%s-%s" % (post.id, suffix_id) - headers = {'Message-ID': msg_id} - elif update == const.TYPE_ACTIVITY_ANSWER: - msg_id = "NA-%s-%s" % (post.id, suffix_id) - orig_id = "NQ-%s-%s" % (orig_post.id, suffix_id) - headers = {'Message-ID': msg_id, - 'In-Reply-To': orig_id} - elif update == const.TYPE_ACTIVITY_UPDATE_QUESTION: - msg_id = "UQ-%s-%s-%s" % (post.id, post.last_edited_at, suffix_id) - orig_id = "NQ-%s-%s" % (orig_post.id, suffix_id) - headers = {'Message-ID': msg_id, - 'In-Reply-To': orig_id} - elif update == const.TYPE_ACTIVITY_COMMENT_QUESTION: - msg_id = "CQ-%s-%s" % (post.id, suffix_id) - orig_id = "NQ-%s-%s" % (orig_post.id, suffix_id) - headers = {'Message-ID': msg_id, - 'In-Reply-To': orig_id} - elif update == const.TYPE_ACTIVITY_UPDATE_ANSWER: - msg_id = "UA-%s-%s-%s" % (post.id, post.last_edited_at, suffix_id) - orig_id = "NQ-%s-%s" % (orig_post.id, suffix_id) - headers = {'Message-ID': msg_id, - 'In-Reply-To': orig_id} - elif update == const.TYPE_ACTIVITY_COMMENT_ANSWER: - msg_id = "CA-%s-%s" % (post.id, suffix_id) - orig_id = "NQ-%s-%s" % (orig_post.id, suffix_id) - headers = {'Message-ID': msg_id, - 'In-Reply-To': orig_id} - else: - # Unknown type -> Can't set headers - return {} - - return headers - -def send_mail( - subject_line = None, - body_text = None, - from_email = django_settings.DEFAULT_FROM_EMAIL, - recipient_list = None, - activity_type = None, - related_object = None, - headers = None, - raise_on_failure = False, - ): - """ - todo: remove parameters not relevant to the function - sends email message - logs email sending activity - and any errors are reported as critical - in the main log file - - related_object is not mandatory, other arguments - are. related_object (if given, will be saved in - the activity record) - - if raise_on_failure is True, exceptions.EmailNotSent is raised - """ - try: - assert(subject_line is not None) - subject_line = prefix_the_subject_line(subject_line) - msg = mail.EmailMessage( - subject_line, - body_text, - from_email, - recipient_list, - headers = headers - ) - msg.content_subtype = 'html' - msg.send() - if related_object is not None: - assert(activity_type is not None) - except Exception, error: - logging.critical(unicode(error)) - if raise_on_failure == True: - raise exceptions.EmailNotSent(unicode(error)) - -def mail_moderators( - subject_line = '', - body_text = '', - raise_on_failure = False): - """sends email to forum moderators and admins - """ - from django.db.models import Q - from askbot.models import User - recipient_list = User.objects.filter( - Q(status='m') | Q(is_superuser=True) - ).filter( - is_active = True - ).values_list('email', flat=True) - recipient_list = set(recipient_list) - - from_email = '' - if hasattr(django_settings, 'DEFAULT_FROM_EMAIL'): - from_email = django_settings.DEFAULT_FROM_EMAIL - - try: - mail.send_mail(subject_line, body_text, from_email, recipient_list) - except smtplib.SMTPException, error: - logging.critical(unicode(error)) - if raise_on_failure == True: - raise exceptions.EmailNotSent(unicode(error)) - -INSTRUCTIONS_PREAMBLE = _('

    To ask by email, please:

    ') -QUESTION_TITLE_INSTRUCTION = _( - '
  • Type title in the subject line
  • ' -) -QUESTION_DETAILS_INSTRUCTION = _( - '
  • Type details of your question into the email body
  • ' -) -OPTIONAL_TAGS_INSTRUCTION = _( -"""
  • The beginning of the subject line can contain tags, -enclosed in the square brackets like so: [Tag1; Tag2]
  • """ -) -REQUIRED_TAGS_INSTRUCTION = _( -"""
  • In the beginning of the subject add at least one tag -enclosed in the brackets like so: [Tag1; Tag2].
  • """ -) -TAGS_INSTRUCTION_FOOTNOTE = _( -"""

    Note that a tag may consist of more than one word, to separate -the tags, use a semicolon or a comma, for example, [One tag; Other tag]

    """ -) - -def bounce_email(email, subject, reason = None, body_text = None): - """sends a bounce email at address ``email``, with the subject - line ``subject``, accepts several reasons for the bounce: - * ``'problem_posting'``, ``unknown_user`` and ``permission_denied`` - * ``body_text`` in an optional parameter that allows to append - extra text to the message - """ - if reason == 'problem_posting': - error_message = _( - '

    Sorry, there was an error posting your question ' - 'please contact the %(site)s administrator

    ' - ) % {'site': askbot_settings.APP_SHORT_NAME} - - if askbot_settings.TAGS_ARE_REQUIRED: - error_message = string_concat( - INSTRUCTIONS_PREAMBLE, - '
      ', - QUESTION_TITLE_INSTRUCTION, - REQUIRED_TAGS_INSTRUCTION, - QUESTION_DETAILS_INSTRUCTION, - '
    ', - TAGS_INSTRUCTION_FOOTNOTE - ) - else: - error_message = string_concat( - INSTRUCTIONS_PREAMBLE, - '
      ', - QUESTION_TITLE_INSTRUCTION, - QUESTION_DETAILS_INSTRUCTION, - OPTIONAL_TAGS_INSTRUCTION, - '
    ', - TAGS_INSTRUCTION_FOOTNOTE - ) - - elif reason == 'unknown_user': - error_message = _( - '

    Sorry, in order to post questions on %(site)s ' - 'by email, please register first

    ' - ) % { - 'site': askbot_settings.APP_SHORT_NAME, - 'url': url_utils.get_login_url() - } - elif reason == 'permission_denied': - error_message = _( - '

    Sorry, your question could not be posted ' - 'due to insufficient privileges of your user account

    ' - ) - else: - raise ValueError('unknown reason to bounce an email: "%s"' % reason) - - if body_text != None: - error_message = string_concat(error_message, body_text) - - #print 'sending email' - #print email - #print subject - #print error_message - send_mail( - recipient_list = (email,), - subject_line = 'Re: ' + subject, - body_text = error_message - ) - -def extract_reply(text): - """take the part above the separator - and discard the last line above the separator""" - if const.REPLY_SEPARATOR_REGEX.search(text): - text = const.REPLY_SEPARATOR_REGEX.split(text)[0] - return '\n'.join(text.splitlines(True)[:-3]) - else: - return text - -def process_attachment(attachment): - """will save a single - attachment and return - link to file in the markdown format and the - file storage object - """ - file_storage, file_name, file_url = store_file(attachment) - markdown_link = '[%s](%s) ' % (attachment.name, file_url) - file_extension = os.path.splitext(attachment.name)[1] - #todo: this is a hack - use content type - if file_extension.lower() in ('.png', '.jpg', '.jpeg', '.gif'): - markdown_link = '!' + markdown_link - return markdown_link, file_storage - -def extract_user_signature(text, reply_code): - """extracts email signature as text trailing - the reply code""" - if reply_code in text: - #extract the signature - tail = list() - for line in reversed(text.splitlines()): - #scan backwards from the end until the magic line - if reply_code in line: - break - tail.insert(0, line) - - #strip off the leading quoted lines, there could be one or two - #also strip empty lines - while tail[0].startswith('>') or tail[0].strip() == '': - tail.pop(0) - - return '\n'.join(tail) - else: - return '' - - -def process_parts(parts, reply_code = None): - """Process parts will upload the attachments and parse out the - body, if body is multipart. Secondly - links to attachments - will be added to the body of the question. - Returns ready to post body of the message and the list - of uploaded files. - """ - body_markdown = '' - stored_files = list() - attachments_markdown = '' - for (part_type, content) in parts: - if part_type == 'attachment': - markdown, stored_file = process_attachment(content) - stored_files.append(stored_file) - attachments_markdown += '\n\n' + markdown - elif part_type == 'body': - body_markdown += '\n\n' + content - elif part_type == 'inline': - markdown, stored_file = process_attachment(content) - stored_files.append(stored_file) - body_markdown += markdown - - #if the response separator is present - - #split the body with it, and discard the "so and so wrote:" part - if reply_code: - signature = extract_user_signature(body_markdown, reply_code) - else: - signature = None - body_markdown = extract_reply(body_markdown) - - body_markdown += attachments_markdown - return body_markdown.strip(), stored_files, signature - - -def process_emailed_question( - from_address, subject, body_text, stored_files, tags = None -): - """posts question received by email or bounces the message""" - #a bunch of imports here, to avoid potential circular import issues - from askbot.forms import AskByEmailForm - from askbot.models import User - - try: - #todo: delete uploaded files when posting by email fails!!! - data = { - 'sender': from_address, - 'subject': subject, - 'body_text': body_text - } - form = AskByEmailForm(data) - if form.is_valid(): - email_address = form.cleaned_data['email'] - user = User.objects.get( - email__iexact = email_address - ) - - if user.email_isvalid == False: - raise PermissionDenied('Lacking email signature') - - tagnames = form.cleaned_data['tagnames'] - title = form.cleaned_data['title'] - body_text = form.cleaned_data['body_text'] - - #defect - here we might get "too many tags" issue - if tags: - tagnames += ' ' + ' '.join(tags) - - stripped_body_text = user.strip_email_signature(body_text) - if stripped_body_text == body_text and user.email_signature: - #todo: send an email asking to update the signature - raise ValueError('email signature changed') - - user.post_question( - title = title, - tags = tagnames.strip(), - body_text = stripped_body_text, - by_email = True, - email_address = from_address - ) - else: - raise ValidationError() - - except User.DoesNotExist: - bounce_email(email_address, subject, reason = 'unknown_user') - except User.MultipleObjectsReturned: - bounce_email(email_address, subject, reason = 'problem_posting') - except PermissionDenied, error: - bounce_email( - email_address, - subject, - reason = 'permission_denied', - body_text = unicode(error) - ) - except ValidationError: - if from_address: - bounce_email( - from_address, - subject, - reason = 'problem_posting', - ) diff --git a/askbot/views/commands.py b/askbot/views/commands.py index 6fd493cc..ec535aef 100644 --- a/askbot/views/commands.py +++ b/askbot/views/commands.py @@ -22,7 +22,7 @@ from askbot.conf import should_show_sort_by_relevance from askbot.conf import settings as askbot_settings from askbot.utils import decorators from askbot.utils import url_utils -from askbot.utils import mail +from askbot import mail from askbot.skins.loaders import render_into_skin, get_template from askbot import const import logging diff --git a/askbot/views/meta.py b/askbot/views/meta.py index 5f4c70ac..b8411b41 100644 --- a/askbot/views/meta.py +++ b/askbot/views/meta.py @@ -14,7 +14,7 @@ from django.views.decorators import csrf from django.db.models import Max, Count from askbot.forms import FeedbackForm from askbot.utils.forms import get_next_url -from askbot.utils.mail import mail_moderators +from askbot.mail import mail_moderators from askbot.models import BadgeData, Award, User from askbot.models import badges as badge_data from askbot.skins.loaders import get_template, render_into_skin, render_text_into_skin diff --git a/askbot/views/users.py b/askbot/views/users.py index a5860b14..a2146a64 100644 --- a/askbot/views/users.py +++ b/askbot/views/users.py @@ -28,7 +28,7 @@ from django.views.decorators import csrf from askbot.utils.slug import slugify from askbot.utils.html import sanitize_html -from askbot.utils.mail import send_mail +from askbot.mail import send_mail from askbot.utils.http import get_request_info from askbot.utils import functions from askbot import forms -- cgit v1.2.3-1-g7c22 From 1e1a8808500d07bb54f90a055f2fab8cf87ca87d Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Fri, 25 May 2012 23:46:29 -0400 Subject: deleted an unused file --- askbot/utils/email.py | 28 ---------------------------- 1 file changed, 28 deletions(-) delete mode 100644 askbot/utils/email.py diff --git a/askbot/utils/email.py b/askbot/utils/email.py deleted file mode 100644 index 34b11c45..00000000 --- a/askbot/utils/email.py +++ /dev/null @@ -1,28 +0,0 @@ -from django.core.mail import EmailMultiAlternatives -from django.conf import settings as django_settings -from django.template import loader, Context -from django.utils.html import strip_tags -from threading import Thread - -def send_email( - subject, - recipients, - template, - context = {}, - sender = django_settings.DEFAULT_FROM_EMAIL, - txt_template = None - ): - context['settings'] = django_settings - html_body = loader.get_template(template).render(Context(context)) - - if txt_template is None: - txt_body = strip_tags(html_body) - else: - txt_body = loader.get_template(txt_template).render(Context(context)) - - msg = EmailMultiAlternatives(subject, txt_body, sender, recipients) - msg.attach_alternative(html_body, "text/html") - - thread = Thread(target=EmailMultiAlternatives.send, args=[msg]) - thread.setDaemon(True) - thread.start() -- cgit v1.2.3-1-g7c22 From ef5ed9c348b8659a156641de99b2a6bfac376d31 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Sat, 26 May 2012 00:18:27 -0400 Subject: a small refactoring of symbols in the post.py --- askbot/models/post.py | 11 ++--------- askbot/tests/post_model_tests.py | 2 +- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/askbot/models/post.py b/askbot/models/post.py index 9d1c9513..2592ee94 100644 --- a/askbot/models/post.py +++ b/askbot/models/post.py @@ -442,7 +442,7 @@ class Post(models.Model): return data #todo: when models are merged, it would be great to remove author parameter - def parse_and_save_post(post, author = None, **kwargs): + def parse_and_save(post, author = None, **kwargs): """generic method to use with posts to be used prior to saving post edit or addition """ @@ -450,7 +450,7 @@ class Post(models.Model): assert(author is not None) last_revision = post.html - data = post.parse() + data = post.parse_post_text() post.html = data['html'] newly_mentioned_users = set(data['newly_mentioned_users']) - set([author]) @@ -496,13 +496,6 @@ class Post(models.Model): except Exception: logging.debug('cannot ping google - did you register with them?') - ###################################### - # TODO: Rename the methods above instead of doing this assignment - parse = parse_post_text - parse_and_save = parse_and_save_post - ###################################### - - def is_question(self): return self.post_type == 'question' diff --git a/askbot/tests/post_model_tests.py b/askbot/tests/post_model_tests.py index dd1399c1..9a4d47c8 100644 --- a/askbot/tests/post_model_tests.py +++ b/askbot/tests/post_model_tests.py @@ -344,7 +344,7 @@ class ThreadRenderLowLevelCachingTests(AskbotTestCase): # Make sure that title and body text are escaped properly. # This should be obvious at this point, if the above test passes, but why not be explicit # UPDATE: And voila, these tests catched double-escaping bug in template, where `<` was `&lt;` - # And indeed, post.summary is escaped before saving, in parse_and_save_post() + # And indeed, post.summary is escaped before saving, in parse_and_save() # UPDATE 2:Weird things happen with question summary (it's double escaped etc., really weird) so # let's just make sure that there are no tag placeholders left self.assertTrue('<<<tag1>>> fake title' in proper_html) -- cgit v1.2.3-1-g7c22 From a3369a5eda129db32009961d75d1624756a4ec29 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Sat, 26 May 2012 14:17:15 -0400 Subject: factored out a function generating email addresses for the email responses --- askbot/models/__init__.py | 163 +++++++++++++++++++++++++--------------- askbot/models/reply_by_email.py | 9 +++ askbot/tasks.py | 4 + 3 files changed, 114 insertions(+), 62 deletions(-) diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py index cba48249..6b425db8 100644 --- a/askbot/models/__init__.py +++ b/askbot/models/__init__.py @@ -2285,7 +2285,11 @@ def user_approve_post_revision(user, post_revision, timestamp = None): post = post_revision.post post.approved = True - post.save() + #counterintuitive, but we need to call parse_and_save() + #because this function extracts newly mentioned users + #and sends the post_updated signal, which ultimately triggers + #sending of the email update + post.parse_and_save(author = post_revision.author) if post_revision.post.post_type == 'question': thread = post.thread thread.approved = True @@ -2572,13 +2576,13 @@ User.add_to_class( user_assert_can_approve_post_revision ) -#todo: move this to askbot/utils ?? +#todo: move this to askbot/mail ? def format_instant_notification_email( to_user = None, from_user = None, post = None, - reply_with_comment_address = None, - reply_code = None, + reply_address = None, + alt_reply_address = None, update_type = None, template = None, ): @@ -2625,13 +2629,13 @@ def format_instant_notification_email( revisions = post.revisions.all()[:2] assert(len(revisions) == 2) content_preview = htmldiff( - revisions[1].html, - revisions[0].html, - ins_start = '', - ins_end = '', - del_start = '', - del_end = '' - ) + revisions[1].html, + revisions[0].html, + ins_start = '', + ins_end = '', + del_start = '', + del_end = '' + ) #todo: remove hardcoded style else: content_preview = post.format_for_email() @@ -2672,12 +2676,13 @@ def format_instant_notification_email( if can_reply: reply_separator = const.REPLY_SEPARATOR_TEMPLATE % { - 'user_action': user_action, - 'instruction': _('To reply, PLEASE WRITE ABOVE THIS LINE.') - } - if post.post_type == 'question' and reply_with_comment_address: - data = {'addr': reply_with_comment_address} - reply_separator += '
    ' + const.REPLY_WITH_COMMENT_TEMPLATE % data + 'user_action': user_action, + 'instruction': _('To reply, PLEASE WRITE ABOVE THIS LINE.') + } + if post.post_type == 'question' and alt_reply_address: + data = {'addr': alt_reply_address} + reply_separator += '
    ' + \ + const.REPLY_WITH_COMMENT_TEMPLATE % data else: reply_separator = user_action @@ -2698,10 +2703,54 @@ def format_instant_notification_email( content = template.render(Context(update_data)) if can_reply: - content += '

    ' + \ + reply_address + '

    ' return subject_line, content +def get_reply_to_addresses(user, post): + """Returns one or two email addresses that can be + used by a given `user` to reply to the `post` + the first address - always a real email address, + the second address is not ``None`` only for "question" posts. + + When the user is notified of a new question - + i.e. `post` is a "quesiton", he/she + will need to choose - whether to give a question or a comment, + thus we return the second address - for the comment reply. + + When the post is a "question", the first email address + is for posting an "answer", and when post is either + "comment" or "answer", the address will be for posting + a "comment". + """ + #these variables will contain return values + primary_addr = django_settings.DEFAULT_FROM_EMAIL + secondary_addr = None + if askbot_settings.REPLY_BY_EMAIL: + if user.reputation >= askbot_settings.MIN_REP_TO_POST_BY_EMAIL: + + reply_args = { + 'post': post, + 'user': user, + 'reply_action': 'post_comment' + } + if post.post_type in ('answer', 'comment'): + reply_args['reply_action'] = 'post_comment' + elif post.post_type == 'question': + reply_args['reply_action'] = 'post_question' + + primary_addr = ReplyAddress.objects.create_new( + **reply_args + ).as_email_address() + + if post.post_type == 'question': + reply_args['reply_action'] = 'post_comment' + secondary_addr = ReplyAddress.objects.create_new( + **reply_args + ).as_email_address() + return primary_addr, secondary_addr + #todo: action def send_instant_notifications_about_activity_in_post( update_activity = None, @@ -2724,64 +2773,31 @@ def send_instant_notifications_about_activity_in_post( if update_activity.activity_type not in acceptable_types: return + #calculate some variables used in the loop below from askbot.skins.loaders import get_template update_type_map = const.RESPONSE_ACTIVITY_TYPE_MAP_FOR_TEMPLATES update_type = update_type_map[update_activity.activity_type] - origin_post = post.get_origin_post() - for user in recipients: - - #todo: this could be packaged as an "action" - a bundle - #of executive function with the activity log recording - #TODO check user reputation - headers = mail.thread_headers(post, origin_post, update_activity.activity_type) - reply_to_with_comment = None#only used for questions in some cases - reply_addr = "noreply" - if askbot_settings.REPLY_BY_EMAIL: - if user.reputation >= askbot_settings.MIN_REP_TO_POST_BY_EMAIL: - - reply_args = { - 'post': post, - 'user': user, - 'reply_action': 'post_comment' - } - if post.post_type in ('answer', 'comment'): - reply_addr = ReplyAddress.objects.create_new( - **reply_args - ).address - reply_to_with_comment = None - elif post.post_type == 'question': - reply_with_comment_address = ReplyAddress.objects.create_new( - **reply_args - ).address - reply_to_with_comment = 'reply-%s@%s' % ( - reply_with_comment_address, - askbot_settings.REPLY_BY_EMAIL_HOSTNAME - ) - #default action is to post answer - reply_args['reply_action'] = 'post_answer' - reply_addr = ReplyAddress.objects.create_new( - **reply_args - ).address - - reply_to = 'reply-%s@%s' % ( - reply_addr, - askbot_settings.REPLY_BY_EMAIL_HOSTNAME + headers = mail.thread_headers( + post, + origin_post, + update_activity.activity_type ) - headers.update({'Reply-To': reply_to}) - else: - reply_to = django_settings.DEFAULT_FROM_EMAIL + #send email for all recipients + for user in recipients: + reply_address, alt_reply_address = get_reply_to_addresses(user, post) subject_line, body_text = format_instant_notification_email( to_user = user, from_user = update_activity.user, post = post, - reply_with_comment_address = reply_to_with_comment, - reply_code = reply_addr, + reply_address = reply_address, + alt_reply_address = alt_reply_address, update_type = update_type, template = get_template('instant_notification.html') ) + headers['Reply-To'] = reply_address mail.send_mail( subject_line = subject_line, body_text = body_text, @@ -2791,6 +2807,29 @@ def send_instant_notifications_about_activity_in_post( headers = headers ) +def send_notification_about_approved_post(post): + """notifies author about approved post, + assumes that we have the very first revision + """ + #for answerable email + if askbot_settings.REPLY_BY_EMAIL: + #generate two reply codes (one for edit and one for addition) + #to format an answerable email or not answerable email + data = { + 'site_name': askbot_settings.APP_SHORT_NAME, + 'post': post + } + from askbot.skins.loaders import get_template + template = get_template('zhopa') + mail.send_mail( + subject_line = _('Your post at %(site_name)s was approved') % data, + body_text = template.render(Context(data)), + recipient_list = [post.author,], + related_object = post, + activity_type = const.TYPE_ACTIVITY_EMAIL_UPDATE_SENT + ) + + #todo: move to utils def calculate_gravatar_hash(instance, **kwargs): """Calculates a User's gravatar hash from their email address.""" diff --git a/askbot/models/reply_by_email.py b/askbot/models/reply_by_email.py index 26111901..4981765f 100644 --- a/askbot/models/reply_by_email.py +++ b/askbot/models/reply_by_email.py @@ -74,6 +74,15 @@ class ReplyAddress(models.Model): """True if was used""" return self.used_at != None + def as_email_address(self, prefix = 'reply-'): + """returns email address, prefix is added + in front of the code""" + return '%s%s@%s' % ( + prefix, + self.address, + askbot_settings.REPLY_BY_EMAIL_HOSTNAME + ) + def edit_post(self, body_text, stored_files): """edits the created post upon repeated response to the same address""" diff --git a/askbot/tasks.py b/askbot/tasks.py index e5ba143d..23966119 100644 --- a/askbot/tasks.py +++ b/askbot/tasks.py @@ -156,6 +156,10 @@ def record_post_update( post = post, recipients = notification_subscribers, ) + #if post.post_type in ('question', 'answer'): + # if created and post.was_moderated(): + # notify_author_about_approved_post(post) + @task(ignore_result = True) def record_question_visit( -- cgit v1.2.3-1-g7c22 From 6ca7dd00282cf76edf80e2c003a2876a78981fdb Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Sun, 27 May 2012 02:38:22 -0400 Subject: test cases for the post approval notifications pass --- askbot/const/__init__.py | 1 + askbot/models/__init__.py | 57 +++++++++++++++++++--- askbot/models/post.py | 46 ++++++++++------- askbot/models/question.py | 2 +- .../email/notify_author_about_approved_post.html | 20 ++++++++ askbot/tasks.py | 10 ++-- askbot/templatetags/extra_filters_jinja.py | 4 ++ askbot/tests/email_alert_tests.py | 44 +++++++++++++++++ askbot/tests/utils.py | 8 ++- 9 files changed, 162 insertions(+), 30 deletions(-) create mode 100644 askbot/skins/default/templates/email/notify_author_about_approved_post.html diff --git a/askbot/const/__init__.py b/askbot/const/__init__.py index ae1d7d2d..e711de92 100644 --- a/askbot/const/__init__.py +++ b/askbot/const/__init__.py @@ -54,6 +54,7 @@ POST_SORT_METHODS = ( POST_TYPES = ('answer', 'comment', 'question', 'tag_wiki', 'reject_reason') +SIMPLE_REPLY_SEPARATOR_TEMPLATE = '==== %s -=-==' REPLY_SEPARATOR_TEMPLATE = '==== %(user_action)s %(instruction)s -=-==' REPLY_WITH_COMMENT_TEMPLATE = _( 'Note: to reply with a comment, ' diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py index 6b425db8..96be2c69 100644 --- a/askbot/models/__init__.py +++ b/askbot/models/__init__.py @@ -2289,7 +2289,8 @@ def user_approve_post_revision(user, post_revision, timestamp = None): #because this function extracts newly mentioned users #and sends the post_updated signal, which ultimately triggers #sending of the email update - post.parse_and_save(author = post_revision.author) + post.parse_and_save(author = post_revision.author, was_approved = True) + if post_revision.post.post_type == 'question': thread = post.thread thread.approved = True @@ -2807,26 +2808,61 @@ def send_instant_notifications_about_activity_in_post( headers = headers ) -def send_notification_about_approved_post(post): +def notify_author_about_approved_post(post): """notifies author about approved post, assumes that we have the very first revision """ - #for answerable email + #for answerable email only for now, because + #we don't yet have the template for the read-only notification if askbot_settings.REPLY_BY_EMAIL: #generate two reply codes (one for edit and one for addition) #to format an answerable email or not answerable email + reply_options = { + 'user': post.author, + 'post': post, + 'reply_action': 'append_content' + } + append_content_address = ReplyAddress.objects.create_new( + **reply_options + ).as_email_address() + reply_options['reply_action'] = 'replace_content' + replace_content_address = ReplyAddress.objects.create_new( + **reply_options + ).as_email_address() + + #populate template context variables + reply_code = append_content_address + ',' + replace_content_address + if post.post_type == 'question': + mailto_link_subject = post.thread.title + else: + mailto_link_subject = _('An edit for my answer') + #todo: possibly add more mailto thread headers to organize messages + + prompt = _('To add to your post EDIT ABOVE THIS LINE') + reply_separator_line = const.SIMPLE_REPLY_SEPARATOR_TEMPLATE % prompt data = { 'site_name': askbot_settings.APP_SHORT_NAME, - 'post': post + 'post': post, + 'replace_content_address': replace_content_address, + 'reply_separator_line': reply_separator_line, + 'mailto_link_subject': mailto_link_subject, + 'reply_code': reply_code } + + #load the template from askbot.skins.loaders import get_template - template = get_template('zhopa') + template = get_template('email/notify_author_about_approved_post.html') + #todo: possibly add headers to organize messages in threads + headers = {'Reply-To': append_content_address} + + #send the message mail.send_mail( subject_line = _('Your post at %(site_name)s was approved') % data, body_text = template.render(Context(data)), - recipient_list = [post.author,], + recipient_list = [post.author.email,], related_object = post, - activity_type = const.TYPE_ACTIVITY_EMAIL_UPDATE_SENT + activity_type = const.TYPE_ACTIVITY_EMAIL_UPDATE_SENT, + headers = headers ) @@ -2845,6 +2881,8 @@ def record_post_update_activity( updated_by = None, timestamp = None, created = False, + was_approved = False, + by_email = False, diff = None, **kwargs ): @@ -2875,6 +2913,11 @@ def record_post_update_activity( created = created, diff = diff, ) + if post.should_notify_author_about_publishing( + was_approved = was_approved, + by_email = by_email + ): + tasks.notify_author_about_approved_post_celery_task.delay(post) #non-celery version #tasks.record_post_update( # post = post, diff --git a/askbot/models/post.py b/askbot/models/post.py index 2592ee94..b98c67a3 100644 --- a/askbot/models/post.py +++ b/askbot/models/post.py @@ -442,51 +442,55 @@ class Post(models.Model): return data #todo: when models are merged, it would be great to remove author parameter - def parse_and_save(post, author = None, **kwargs): + def parse_and_save( + self, author = None, was_approved = False, by_email = False, **kwargs + ): """generic method to use with posts to be used prior to saving post edit or addition """ assert(author is not None) - last_revision = post.html - data = post.parse_post_text() + last_revision = self.html + data = self.parse_post_text() - post.html = data['html'] + self.html = data['html'] newly_mentioned_users = set(data['newly_mentioned_users']) - set([author]) removed_mentions = data['removed_mentions'] #a hack allowing to save denormalized .summary field for questions - if hasattr(post, 'summary'): - post.summary = post.get_snippet() + if hasattr(self, 'summary'): + self.summary = self.get_snippet() #delete removed mentions for rm in removed_mentions: rm.delete() - created = post.pk is None + created = self.pk is None #this save must precede saving the mention activity #because generic relation needs primary key of the related object - super(post.__class__, post).save(**kwargs) + super(self.__class__, self).save(**kwargs) if last_revision: - diff = htmldiff(last_revision, post.html) + diff = htmldiff(last_revision, self.html) else: - diff = post.get_snippet() + diff = self.get_snippet() - timestamp = post.get_time_of_last_edit() + timestamp = self.get_time_of_last_edit() #todo: this is handled in signal because models for posts #are too spread out from askbot.models import signals signals.post_updated.send( - post = post, + post = self, updated_by = author, newly_mentioned_users = newly_mentioned_users, timestamp = timestamp, created = created, + by_email = by_email, + was_approved = was_approved, diff = diff, - sender = post.__class__ + sender = self.__class__ ) try: @@ -514,6 +518,17 @@ class Post(models.Model): def needs_moderation(self): return self.approved == False + def should_notify_author_about_publishing( + self, was_approved = False, by_email = False + ): + """True if post is a newly published question or answer + which was posted by email and or just approved by the + moderator""" + if self.post_type in ('question', 'answer'): + if self.revisions.count() == 0:#brand new post + return was_approved or by_email + return False + def get_absolute_url(self, no_slug = False, question_post=None, thread=None): from askbot.utils.slug import slugify #todo: the url generation function is pretty bad - @@ -1048,10 +1063,7 @@ class Post(models.Model): return self.revisions.order_by('-revised_at')[0] def get_latest_revision_number(self): - if self.is_comment(): - return 1 - else: - return self.get_latest_revision().revision + return self.get_latest_revision().revision def get_time_of_last_edit(self): if self.is_comment(): diff --git a/askbot/models/question.py b/askbot/models/question.py index a18e719b..795ecfed 100644 --- a/askbot/models/question.py +++ b/askbot/models/question.py @@ -107,7 +107,7 @@ class ThreadManager(models.Manager): question.last_edited_at = added_at question.wikified_at = added_at - question.parse_and_save(author = author) + question.parse_and_save(author = author, by_email = by_email) question.add_revision( author = author, diff --git a/askbot/skins/default/templates/email/notify_author_about_approved_post.html b/askbot/skins/default/templates/email/notify_author_about_approved_post.html new file mode 100644 index 00000000..6083afc1 --- /dev/null +++ b/askbot/skins/default/templates/email/notify_author_about_approved_post.html @@ -0,0 +1,20 @@ +{# + parameters: + * reply_separator_line + * replace_content_address + * mailto_link_subject + * post + * reply_code (comma-separated list of emails to respond to this message) +#} +{{ reply_separator_line }} +

    {% trans + post_text=post.text|safe_urlquote, + subject=mailto_link_subject|safe_urlquote +%}If you would like to edit by email, please +click here{% endtrans %}

    +

    {% trans %}Below is a copy of your post:{% endtrans %}

    +{% if post.post_type == 'question' %} +

    {{ post.thread.title }}

    +{% endif %} +{{ post.html }} +

    {{ reply_code }}

    diff --git a/askbot/tasks.py b/askbot/tasks.py index 23966119..447b6b78 100644 --- a/askbot/tasks.py +++ b/askbot/tasks.py @@ -25,12 +25,17 @@ from celery.decorators import task from askbot.conf import settings as askbot_settings from askbot.models import Activity, Post, Thread, User from askbot.models import send_instant_notifications_about_activity_in_post +from askbot.models import notify_author_about_approved_post from askbot.models.badges import award_badges_signal # TODO: Make exceptions raised inside record_post_update_celery_task() ... # ... propagate upwards to test runner, if only CELERY_ALWAYS_EAGER = True # (i.e. if Celery tasks are not deferred but executed straight away) +@task(ignore_result = True) +def notify_author_about_approved_post_celery_task(post): + notify_author_about_approved_post(post) + @task(ignore_result = True) def record_post_update_celery_task( post_id, @@ -156,10 +161,7 @@ def record_post_update( post = post, recipients = notification_subscribers, ) - #if post.post_type in ('question', 'answer'): - # if created and post.was_moderated(): - # notify_author_about_approved_post(post) - + @task(ignore_result = True) def record_question_visit( diff --git a/askbot/templatetags/extra_filters_jinja.py b/askbot/templatetags/extra_filters_jinja.py index b03e4a89..fa9d0ced 100644 --- a/askbot/templatetags/extra_filters_jinja.py +++ b/askbot/templatetags/extra_filters_jinja.py @@ -46,6 +46,10 @@ TIMEZONE_STR = pytz.timezone( def add_tz_offset(datetime_object): return str(datetime_object) + ' ' + TIMEZONE_STR +@register.filter +def safe_urlquote(text): + return urllib.quote_plus(text.encode('utf8')) + @register.filter def strip_path(url): """removes path part of the url""" diff --git a/askbot/tests/email_alert_tests.py b/askbot/tests/email_alert_tests.py index 828b341b..5f20496d 100644 --- a/askbot/tests/email_alert_tests.py +++ b/askbot/tests/email_alert_tests.py @@ -949,3 +949,47 @@ class EmailFeedSettingTests(utils.AskbotTestCase): new_user.add_missing_askbot_subscriptions() data_after = TO_JSON(self.get_user_feeds()) self.assertEquals(data_before, data_after) + +class PostApprovalTests(utils.AskbotTestCase): + """test notifications sent to authors when their posts + are approved or published""" + def setUp(self): + assert( + django_settings.EMAIL_BACKEND == 'django.core.mail.backends.locmem.EmailBackend' + ) + + def test_emailed_question_answerable_approval_notification(self): + setting_backup = askbot_settings.REPLY_BY_EMAIL + askbot_settings.update('REPLY_BY_EMAIL', True) + self.u1 = self.create_user('user1', status = 'a') + question = self.post_question(user = self.u1, by_email = True) + outbox = django.core.mail.outbox + self.assertEquals(len(outbox), 1) + self.assertEquals(outbox[0].recipients(), [self.u1.email]) + askbot_settings.update('REPLY_BY_EMAIL', setting_backup) + + def test_moderated_question_answerable_approval_notification(self): + setting_backup1 = askbot_settings.REPLY_BY_EMAIL + askbot_settings.update('REPLY_BY_EMAIL', True) + setting_backup2 = askbot_settings.ENABLE_CONTENT_MODERATION + askbot_settings.update('ENABLE_CONTENT_MODERATION', True) + + u1 = self.create_user('user1', status = 'a') + question = self.post_question(user = u1, by_email = True) + + self.assertEquals(question.approved, False) + + u2 = self.create_user('admin', status = 'd') + + self.assertEquals(question.revisions.count(), 1) + u2.approve_post_revision(question.get_latest_revision()) + + outbox = django.core.mail.outbox + self.assertEquals(len(outbox), 2) + #moderation notification + self.assertEquals(outbox[0].recipients(), [u1.email,]) + self.assertEquals(outbox[1].recipients(), [u1.email,])#approval + + askbot_settings.update('REPLY_BY_EMAIL', setting_backup1) + askbot_settings.update('ENABLE_CONTENT_MODERATION', setting_backup2) + diff --git a/askbot/tests/utils.py b/askbot/tests/utils.py index fdeea371..8395b1ca 100644 --- a/askbot/tests/utils.py +++ b/askbot/tests/utils.py @@ -115,10 +115,11 @@ class AskbotTestCase(TestCase): title = 'test question title', body_text = 'test question body text', tags = 'test', + by_email = False, wiki = False, is_anonymous = False, follow = False, - timestamp = None + timestamp = None, ): """posts and returns question on behalf of user. If user is not given, it will be self.user @@ -135,6 +136,7 @@ class AskbotTestCase(TestCase): title = title, body_text = body_text, tags = tags, + by_email = by_email, wiki = wiki, is_anonymous = is_anonymous, timestamp = timestamp @@ -155,6 +157,7 @@ class AskbotTestCase(TestCase): user = None, question = None, body_text = 'test answer text', + by_email = False, follow = False, wiki = False, timestamp = None @@ -165,6 +168,7 @@ class AskbotTestCase(TestCase): return user.post_answer( question = question, body_text = body_text, + by_email = by_email, follow = follow, wiki = wiki, timestamp = timestamp @@ -175,6 +179,7 @@ class AskbotTestCase(TestCase): user = None, parent_post = None, body_text = 'test comment text', + by_email = False, timestamp = None ): """posts and returns a comment to parent post, uses @@ -187,6 +192,7 @@ class AskbotTestCase(TestCase): comment = user.post_comment( parent_post = parent_post, body_text = body_text, + by_email = by_email, timestamp = timestamp, ) -- cgit v1.2.3-1-g7c22 From cc62cf46af63cb65afb8e322a5ebb86462d6fb45 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Sun, 27 May 2012 14:36:32 -0400 Subject: hide website url and the "about" sections of the blocked user profiles, blocked users are not present in the list of the users either, website url does not show in post signatures --- askbot/doc/source/changelog.rst | 5 +++++ askbot/models/__init__.py | 8 ++++++++ askbot/models/question.py | 14 ++++++++++++++ askbot/skins/default/templates/macros.html | 2 +- askbot/skins/default/templates/user_profile/user_info.html | 4 ++-- .../default/templates/user_profile/user_moderate.html | 2 +- askbot/views/users.py | 2 +- 7 files changed, 32 insertions(+), 5 deletions(-) diff --git a/askbot/doc/source/changelog.rst b/askbot/doc/source/changelog.rst index e005e019..e9c0da6a 100644 --- a/askbot/doc/source/changelog.rst +++ b/askbot/doc/source/changelog.rst @@ -1,6 +1,11 @@ Changes in Askbot ================= +In development +--------------------- +* Hide "website" and "about" section of the blocked user profiles + to help prevent user profile spam. (Evgeny) + 0.7.43 (May 14, 2012) --------------------- * User groups (Evgeny) diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py index 5e41a07a..27bc98c2 100644 --- a/askbot/models/__init__.py +++ b/askbot/models/__init__.py @@ -1841,6 +1841,14 @@ def user_set_status(self, new_status): if self.is_administrator(): self.remove_admin_status() + #when toggling between blocked and non-blocked status + #we need to invalidate question page caches, b/c they contain + #user's url, which must be hidden in the blocked state + if 'b' in (new_status, self.status) and new_status != self.status: + threads = Thread.objects.get_for_user(self) + for thread in threads: + thread.invalidate_cached_post_data() + self.status = new_status self.save() diff --git a/askbot/models/question.py b/askbot/models/question.py index 7d1c3758..dafe7ab0 100644 --- a/askbot/models/question.py +++ b/askbot/models/question.py @@ -357,6 +357,20 @@ class ThreadManager(models.Manager): contributors = User.objects.filter(id__in=u_id).order_by('avatar_type', '?')[:avatar_limit] return contributors + def get_for_user(self, user): + """returns threads where a given user had participated""" + post_ids = PostRevision.objects.filter( + author = user + ).values_list( + 'post_id', flat = True + ).distinct() + thread_ids = Post.objects.filter( + id__in = post_ids + ).values_list( + 'thread_id', flat = True + ).distinct() + return self.filter(id__in = thread_ids) + class Thread(models.Model): SUMMARY_CACHE_KEY_TPL = 'thread-question-summary-%d' diff --git a/askbot/skins/default/templates/macros.html b/askbot/skins/default/templates/macros.html index 4bae1e45..c8dc245f 100644 --- a/askbot/skins/default/templates/macros.html +++ b/askbot/skins/default/templates/macros.html @@ -578,7 +578,7 @@ answer {% if answer.accepted() %}accepted-answer{% endif %} {% if answer.author_ {%- endmacro -%} {%- macro user_website_link(user, max_display_length=25) -%} - {% if user.website %} + {% if user.website and (not user.is_blocked()) %} {{ macros.timeago(view_user.last_seen) }} {% endif %} - {% if view_user.website %} + {% if view_user.website and (not view_user.is_blocked()) %} {% trans %}website{% endtrans %} {{ macros.user_website_link(view_user, max_display_length = 30) }} @@ -95,7 +95,7 @@
    - {% if view_user.about %} + {% if view_user.about and (not view_user.is_blocked()) %} {{view_user.about|linebreaks}} {% endif %}
    diff --git a/askbot/skins/default/templates/user_profile/user_moderate.html b/askbot/skins/default/templates/user_profile/user_moderate.html index 048f35b4..347ec3af 100644 --- a/askbot/skins/default/templates/user_profile/user_moderate.html +++ b/askbot/skins/default/templates/user_profile/user_moderate.html @@ -83,7 +83,7 @@ $('#id_user_status_info').html("{% trans %}Suspended users can only edit or delete their own posts.{% endtrans %}"); $('#id_user_status_info').show('slow'); } else if (optionValue == "b"){ - $('#id_user_status_info').html("{% trans %}Blocked users can only login and send feedback to the site administrators.{% endtrans %}"); + $('#id_user_status_info').html("{% trans %}Blocked users can only login and send feedback to the site administrators, their url and profile will also be hidden.{% endtrans %}"); $('#id_user_status_info').show('slow'); } else { $('#id_user_status_info').hide('slow'); diff --git a/askbot/views/users.py b/askbot/views/users.py index ef0aea57..c17ddd43 100644 --- a/askbot/views/users.py +++ b/askbot/views/users.py @@ -57,7 +57,7 @@ def owner_or_moderator_required(f): def users(request, by_group = False, group_id = None, group_slug = None): """Users view, including listing of users by group""" - users = models.User.objects.all() + users = models.User.objects.exclude(status = 'b') group = None group_email_moderation_enabled = False user_can_join_group = False -- cgit v1.2.3-1-g7c22 From 098b8335dcdbe236d9d82e65126b6010f5ece3aa Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Sun, 27 May 2012 18:50:47 -0400 Subject: a possibility to add a custom user profile tab --- askbot/doc/source/changelog.rst | 4 +- askbot/doc/source/optional-modules.rst | 43 ++++++++++++++++++++++ .../default/templates/user_profile/custom_tab.html | 3 ++ .../skins/default/templates/user_profile/user.html | 2 +- .../default/templates/user_profile/user_tabs.html | 5 +++ askbot/startup_procedures.py | 32 ++++++++++++++++ askbot/views/users.py | 28 ++++++++++++++ 7 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 askbot/skins/default/templates/user_profile/custom_tab.html diff --git a/askbot/doc/source/changelog.rst b/askbot/doc/source/changelog.rst index e9c0da6a..7d1a2fc0 100644 --- a/askbot/doc/source/changelog.rst +++ b/askbot/doc/source/changelog.rst @@ -4,7 +4,9 @@ Changes in Askbot In development --------------------- * Hide "website" and "about" section of the blocked user profiles - to help prevent user profile spam. (Evgeny) + to help prevent user profile spam (Evgeny) +* Added a function to create a custom user profile tab, + the feature requires access to the server (Evgeny) 0.7.43 (May 14, 2012) --------------------- diff --git a/askbot/doc/source/optional-modules.rst b/askbot/doc/source/optional-modules.rst index 0c013121..6e211f4d 100644 --- a/askbot/doc/source/optional-modules.rst +++ b/askbot/doc/source/optional-modules.rst @@ -124,6 +124,49 @@ Also, settings ``MEDIA_ROOT`` and ``MEDIA_URL`` will need to be added to your `` be up to date, so please take the development version from the github repository +Custom section in the user profile +================================== +Sometimes you might want to add a completely custom section +to the user profile, available via an additional tab. + +This is possible by editing the ``settings.py`` file, +which means that to use this feature you must have sufficient +access to the webserver file system. + +Add a following setting to your ``settings.py``:: + + ASKBOT_CUSTOM_USER_PROFILE_TAB = { + 'NAME': 'some name', + 'SLUG': 'some-name', + 'CONTENT_GENERATOR': 'myapp.views.somefunc' + } + +The value of ``ASKBOT_CUSTOM_USER_PROFILE_TAB['CONTENT_GENERATOR']`` +should be a path to the function that returns the widget content +as string. + +Here is a simple example of the content generator +implemented as part of the fictional application called ``myapp``:: + + from myapp.models import Thing#definition not shown here + from django.template.loader import get_template + from django.template import Context + + def somefunc(request, profile_owner): + """loads things for the ``profile_owner`` + and returns output rendered as html string + """ + template = get_template('mytemplate.html') + things = Thing.objects.filter(user = profile_owner) + return template.render(Context({'things': things})) + +The function is very similar to the regular +Django view, but returns a string instead of the ``HttpResponse`` +instance. + +Also, the method must accept one additional argument - +an instance of the ``django.contrib.auth.models.User`` object. + Wordpress Integration ===================== diff --git a/askbot/skins/default/templates/user_profile/custom_tab.html b/askbot/skins/default/templates/user_profile/custom_tab.html new file mode 100644 index 00000000..bc5647f7 --- /dev/null +++ b/askbot/skins/default/templates/user_profile/custom_tab.html @@ -0,0 +1,3 @@ +{% extends "user_profile/user.html" %} +{% block profilesection %}{{ custom_tab_name }}{% endblock %} +{% block usercontent %}{{ custom_tab_content|safe }}{% endblock %} diff --git a/askbot/skins/default/templates/user_profile/user.html b/askbot/skins/default/templates/user_profile/user.html index 15e0622a..dfe9b787 100644 --- a/askbot/skins/default/templates/user_profile/user.html +++ b/askbot/skins/default/templates/user_profile/user.html @@ -1,7 +1,7 @@ {% extends "one_column_body.html" %} {% block title %}{% spaceless %}{{ page_title }}{% endspaceless %}{% endblock %} -{% block forestyle%} +{% block forestyle %} diff --git a/askbot/skins/default/templates/user_profile/user_tabs.html b/askbot/skins/default/templates/user_profile/user_tabs.html index b8c56479..c7df4187 100644 --- a/askbot/skins/default/templates/user_profile/user_tabs.html +++ b/askbot/skins/default/templates/user_profile/user_tabs.html @@ -49,6 +49,11 @@ href="{% url user_profile view_user.id, view_user.username|slugify %}?sort=moderation" >{% trans %}moderation{% endtrans %}
    {% endif %} + {% if custom_tab_slug %} + {{ custom_tab_name }} + {% endif %}
    diff --git a/askbot/startup_procedures.py b/askbot/startup_procedures.py index a5b0c940..0fec6d5f 100644 --- a/askbot/startup_procedures.py +++ b/askbot/startup_procedures.py @@ -10,6 +10,7 @@ the main function is run_startup_tests import sys import os import re +import urllib import askbot import south from django.db import transaction, connection @@ -481,6 +482,36 @@ def test_avatar(): short_message = True ) +def test_custom_user_profile_tab(): + setting_name = 'ASKBOT_CUSTOM_USER_PROFILE_TAB' + tab_settings = getattr(django_settings, setting_name, None) + if tab_settings: + if not isinstance(tab_settings, dict): + print "Setting %s must be a dictionary!!!" % setting_name + + name = tab_settings.get('NAME', None) + slug = tab_settings.get('SLUG', None) + func_name = tab_settings.get('CONTENT_GENERATOR', None) + + errors = list() + if (name is None) or (not(isinstance(name, basestring))): + errors.append("%s['NAME'] must be a string" % setting_name) + if (slug is None) or (not(isinstance(slug, str))): + errors.append("%s['SLUG'] must be an ASCII string" % setting_name) + + if urllib.quote_plus(slug) != slug: + errors.append( + "%s['SLUG'] must be url safe, make it simple" % setting_name + ) + + try: + func = load_module(func_name) + except ImportError: + errors.append("%s['CONTENT_GENERATOR'] must be a dotted path to a function" % setting_name) + header = 'Custom user profile tab is configured incorrectly in your settings.py file' + footer = 'Please carefully read about adding a custom user profile tab.' + print_errors(errors, header = header, footer = footer) + def run_startup_tests(): """function that runs all startup tests, mainly checking settings config so far @@ -532,6 +563,7 @@ def run_startup_tests(): test_media_url() if 'manage.py test' in ' '.join(sys.argv): test_settings_for_test_runner() + test_custom_user_profile_tab() @transaction.commit_manually def run(): diff --git a/askbot/views/users.py b/askbot/views/users.py index c17ddd43..0997f21e 100644 --- a/askbot/views/users.py +++ b/askbot/views/users.py @@ -41,6 +41,7 @@ from askbot.skins.loaders import render_into_skin from askbot.templatetags import extra_tags from askbot.search.state_manager import SearchState from askbot.utils import url_utils +from askbot.utils.loading import load_module def owner_or_moderator_required(f): @functools.wraps(f) @@ -840,6 +841,24 @@ def user_email_subscriptions(request, user, context): request ) +@csrf.csrf_protect +def user_custom_tab(request, user, context): + """works only if `ASKBOT_CUSTOM_USER_PROFILE_TAB` + setting in the ``settings.py`` is properly configured""" + tab_settings = django_settings.ASKBOT_CUSTOM_USER_PROFILE_TAB + module_path = tab_settings['CONTENT_GENERATOR'] + content_generator = load_module(module_path) + + page_title = _('profile - %(section)s') % \ + {'section': tab_settings['NAME']} + + context.update({ + 'custom_tab_content': content_generator(request, user), + 'tab_name': tab_settings['SLUG'], + 'page_title': page_title + }) + return render_into_skin('user_profile/custom_tab.html', context, request) + USER_VIEW_CALL_TABLE = { 'stats': user_stats, 'recent': user_recent, @@ -851,6 +870,12 @@ USER_VIEW_CALL_TABLE = { 'email_subscriptions': user_email_subscriptions, 'moderation': user_moderate, } + +CUSTOM_TAB = getattr(django_settings, 'ASKBOT_CUSTOM_USER_PROFILE_TAB', None) +if CUSTOM_TAB: + CUSTOM_SLUG = CUSTOM_TAB['SLUG'] + USER_VIEW_CALL_TABLE[CUSTOM_SLUG] = user_custom_tab + #todo: rename this function - variable named user is everywhere def user(request, id, slug=None, tab_name=None): """Main user view function that works as a switchboard @@ -897,6 +922,9 @@ def user(request, id, slug=None, tab_name=None): 'search_state': search_state, 'user_follow_feature_on': ('followit' in django_settings.INSTALLED_APPS), } + if CUSTOM_TAB: + context['custom_tab_name'] = CUSTOM_TAB['NAME'] + context['custom_tab_slug'] = CUSTOM_TAB['SLUG'] return user_view_func(request, profile_owner, context) @csrf.csrf_exempt -- cgit v1.2.3-1-g7c22 From 12382d0eecbd8228e3dc47c2a419371b22507927 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Mon, 28 May 2012 01:20:41 -0400 Subject: fixed a bug in processing of ask by email --- askbot/mail/lamson_handlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/askbot/mail/lamson_handlers.py b/askbot/mail/lamson_handlers.py index a802fbb6..afcbb031 100644 --- a/askbot/mail/lamson_handlers.py +++ b/askbot/mail/lamson_handlers.py @@ -188,7 +188,7 @@ def ASK(message, host = None, addr = None): parts = get_parts(message) from_address = message.From subject = message['Subject']#why lamson does not give it normally? - body_text, stored_files, unused = process_parts(parts) + body_text, stored_files, unused = mail.process_parts(parts) if addr == 'ask': mail.process_emailed_question( from_address, subject, body_text, stored_files -- cgit v1.2.3-1-g7c22 From 1815e7a2f5b01652a19caeddd2ab0cfb63fe9139 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Mon, 28 May 2012 06:22:56 -0400 Subject: hopefully fixed bugs with the approval notification messages --- askbot/models/__init__.py | 96 ++++-------------------------------- askbot/models/post.py | 36 +++++++------- askbot/models/question.py | 2 +- askbot/models/signals.py | 1 + askbot/tasks.py | 61 +++++++++++++++++++++-- askbot/tests/email_alert_tests.py | 30 ++++++----- askbot/tests/reply_by_email_tests.py | 2 +- 7 files changed, 105 insertions(+), 123 deletions(-) diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py index 96be2c69..376c7356 100644 --- a/askbot/models/__init__.py +++ b/askbot/models/__init__.py @@ -2285,11 +2285,7 @@ def user_approve_post_revision(user, post_revision, timestamp = None): post = post_revision.post post.approved = True - #counterintuitive, but we need to call parse_and_save() - #because this function extracts newly mentioned users - #and sends the post_updated signal, which ultimately triggers - #sending of the email update - post.parse_and_save(author = post_revision.author, was_approved = True) + post.save() if post_revision.post.post_type == 'question': thread = post.thread @@ -2297,6 +2293,9 @@ def user_approve_post_revision(user, post_revision, timestamp = None): thread.save() post.thread.invalidate_cached_data() + #send the signal of published revision + signals.post_revision_published.send(None, revision = post_revision) + @auto_now_timestamp def flag_post(user, post, timestamp=None, cancel=False, cancel_all = False, force = False): if cancel_all: @@ -2808,62 +2807,13 @@ def send_instant_notifications_about_activity_in_post( headers = headers ) -def notify_author_about_approved_post(post): - """notifies author about approved post, +def notify_author_of_published_revision(revision, **kwargs): + """notifies author about approved post revision, assumes that we have the very first revision """ - #for answerable email only for now, because - #we don't yet have the template for the read-only notification - if askbot_settings.REPLY_BY_EMAIL: - #generate two reply codes (one for edit and one for addition) - #to format an answerable email or not answerable email - reply_options = { - 'user': post.author, - 'post': post, - 'reply_action': 'append_content' - } - append_content_address = ReplyAddress.objects.create_new( - **reply_options - ).as_email_address() - reply_options['reply_action'] = 'replace_content' - replace_content_address = ReplyAddress.objects.create_new( - **reply_options - ).as_email_address() - - #populate template context variables - reply_code = append_content_address + ',' + replace_content_address - if post.post_type == 'question': - mailto_link_subject = post.thread.title - else: - mailto_link_subject = _('An edit for my answer') - #todo: possibly add more mailto thread headers to organize messages - - prompt = _('To add to your post EDIT ABOVE THIS LINE') - reply_separator_line = const.SIMPLE_REPLY_SEPARATOR_TEMPLATE % prompt - data = { - 'site_name': askbot_settings.APP_SHORT_NAME, - 'post': post, - 'replace_content_address': replace_content_address, - 'reply_separator_line': reply_separator_line, - 'mailto_link_subject': mailto_link_subject, - 'reply_code': reply_code - } - - #load the template - from askbot.skins.loaders import get_template - template = get_template('email/notify_author_about_approved_post.html') - #todo: possibly add headers to organize messages in threads - headers = {'Reply-To': append_content_address} - - #send the message - mail.send_mail( - subject_line = _('Your post at %(site_name)s was approved') % data, - body_text = template.render(Context(data)), - recipient_list = [post.author.email,], - related_object = post, - activity_type = const.TYPE_ACTIVITY_EMAIL_UPDATE_SENT, - headers = headers - ) + if revision.revision == 1:#only email about first revision + from askbot.tasks import notify_author_of_published_revision_celery_task + notify_author_of_published_revision_celery_task.delay(revision) #todo: move to utils @@ -2881,8 +2831,6 @@ def record_post_update_activity( updated_by = None, timestamp = None, created = False, - was_approved = False, - by_email = False, diff = None, **kwargs ): @@ -2913,19 +2861,6 @@ def record_post_update_activity( created = created, diff = diff, ) - if post.should_notify_author_about_publishing( - was_approved = was_approved, - by_email = by_email - ): - tasks.notify_author_about_approved_post_celery_task.delay(post) - #non-celery version - #tasks.record_post_update( - # post = post, - # newly_mentioned_users = newly_mentioned_users, - # updated_by = updated_by, - # timestamp = timestamp, - # created = created, - #) def record_award_event(instance, created, **kwargs): @@ -3270,22 +3205,10 @@ def make_admin_if_first_user(instance, **kwargs): instance.set_admin_status() cache.cache.set('admin-created', True) -def place_post_revision_on_moderation_queue(instance, **kwargs): - """`instance` is post revision, because we must - be able to moderate all the revisions, if necessary, - in order to avoid people getting the post past the moderation - then make some evil edit. - """ - if instance.needs_moderation(): - instance.place_on_moderation_queue() #signal for User model save changes django_signals.pre_save.connect(make_admin_if_first_user, sender=User) django_signals.pre_save.connect(calculate_gravatar_hash, sender=User) -django_signals.post_save.connect( - place_post_revision_on_moderation_queue, - sender=PostRevision -) django_signals.post_save.connect(add_missing_subscriptions, sender=User) django_signals.post_save.connect(record_award_event, sender=Award) django_signals.post_save.connect(notify_award_message, sender=Award) @@ -3310,6 +3233,7 @@ signals.user_updated.connect(record_user_full_updated, sender=User) signals.user_logged_in.connect(complete_pending_tag_subscriptions)#todo: add this to fake onlogin middleware signals.user_logged_in.connect(post_anonymous_askbot_content) signals.post_updated.connect(record_post_update_activity) +signals.post_revision_published.connect(notify_author_of_published_revision) signals.site_visited.connect(record_user_visit) #set up a possibility for the users to follow others diff --git a/askbot/models/post.py b/askbot/models/post.py index b98c67a3..0bc57ebb 100644 --- a/askbot/models/post.py +++ b/askbot/models/post.py @@ -442,9 +442,7 @@ class Post(models.Model): return data #todo: when models are merged, it would be great to remove author parameter - def parse_and_save( - self, author = None, was_approved = False, by_email = False, **kwargs - ): + def parse_and_save(self, author = None, **kwargs): """generic method to use with posts to be used prior to saving post edit or addition """ @@ -487,8 +485,6 @@ class Post(models.Model): newly_mentioned_users = newly_mentioned_users, timestamp = timestamp, created = created, - by_email = by_email, - was_approved = was_approved, diff = diff, sender = self.__class__ ) @@ -518,17 +514,6 @@ class Post(models.Model): def needs_moderation(self): return self.approved == False - def should_notify_author_about_publishing( - self, was_approved = False, by_email = False - ): - """True if post is a newly published question or answer - which was posted by email and or just approved by the - moderator""" - if self.post_type in ('question', 'answer'): - if self.revisions.count() == 0:#brand new post - return was_approved or by_email - return False - def get_absolute_url(self, no_slug = False, question_post=None, thread=None): from askbot.utils.slug import slugify #todo: the url generation function is pretty bad - @@ -1682,11 +1667,15 @@ class PostRevisionManager(models.Manager): def create_question_revision(self, *kargs, **kwargs): kwargs['revision_type'] = self.model.QUESTION_REVISION - return super(PostRevisionManager, self).create(*kargs, **kwargs) + revision = super(PostRevisionManager, self).create(*kargs, **kwargs) + revision.moderate_or_publish() + return revision def create_answer_revision(self, *kargs, **kwargs): kwargs['revision_type'] = self.model.ANSWER_REVISION - return super(PostRevisionManager, self).create(*kargs, **kwargs) + revision = super(PostRevisionManager, self).create(*kargs, **kwargs) + revision.moderate_or_publish() + return revision def question_revisions(self): return self.filter(revision_type=self.model.QUESTION_REVISION) @@ -1829,6 +1818,17 @@ class PostRevision(models.Model): #todo: make this group-sensitive activity.add_recipients(get_admins_and_moderators()) + + def moderate_or_publish(self): + """either place on moderation queue or announce + that this revision is published""" + if self.needs_moderation():#moderate + self.place_on_moderation_queue() + else:#auto-approve + from askbot.models import signals + signals.post_revision_published.send(None, revision = self) + + def revision_type_str(self): return self.REVISION_TYPE_CHOICES_DICT[self.revision_type] diff --git a/askbot/models/question.py b/askbot/models/question.py index 795ecfed..a18e719b 100644 --- a/askbot/models/question.py +++ b/askbot/models/question.py @@ -107,7 +107,7 @@ class ThreadManager(models.Manager): question.last_edited_at = added_at question.wikified_at = added_at - question.parse_and_save(author = author, by_email = by_email) + question.parse_and_save(author = author) question.add_revision( author = author, diff --git a/askbot/models/signals.py b/askbot/models/signals.py index 28fe70b0..1541eff5 100644 --- a/askbot/models/signals.py +++ b/askbot/models/signals.py @@ -32,6 +32,7 @@ post_updated = django.dispatch.Signal( 'newly_mentioned_users' ] ) +post_revision_published = django.dispatch.Signal(providing_args = ['revision']) site_visited = django.dispatch.Signal(providing_args=['user', 'timestamp']) def pop_signal_receivers(signal): diff --git a/askbot/tasks.py b/askbot/tasks.py index 447b6b78..71719ef3 100644 --- a/askbot/tasks.py +++ b/askbot/tasks.py @@ -21,11 +21,14 @@ import sys import traceback from django.contrib.contenttypes.models import ContentType +from django.template import Context +from django.utils.translation import ugettext as _ from celery.decorators import task from askbot.conf import settings as askbot_settings -from askbot.models import Activity, Post, Thread, User +from askbot import const +from askbot import mail +from askbot.models import Activity, Post, Thread, User, ReplyAddress from askbot.models import send_instant_notifications_about_activity_in_post -from askbot.models import notify_author_about_approved_post from askbot.models.badges import award_badges_signal # TODO: Make exceptions raised inside record_post_update_celery_task() ... @@ -33,8 +36,58 @@ from askbot.models.badges import award_badges_signal # (i.e. if Celery tasks are not deferred but executed straight away) @task(ignore_result = True) -def notify_author_about_approved_post_celery_task(post): - notify_author_about_approved_post(post) +def notify_author_of_published_revision_celery_task(revision): + #for answerable email only for now, because + #we don't yet have the template for the read-only notification + if askbot_settings.REPLY_BY_EMAIL: + #generate two reply codes (one for edit and one for addition) + #to format an answerable email or not answerable email + reply_options = { + 'user': revision.author, + 'post': revision.post, + 'reply_action': 'append_content' + } + append_content_address = ReplyAddress.objects.create_new( + **reply_options + ).as_email_address() + reply_options['reply_action'] = 'replace_content' + replace_content_address = ReplyAddress.objects.create_new( + **reply_options + ).as_email_address() + + #populate template context variables + reply_code = append_content_address + ',' + replace_content_address + if revision.post.post_type == 'question': + mailto_link_subject = revision.post.thread.title + else: + mailto_link_subject = _('An edit for my answer') + #todo: possibly add more mailto thread headers to organize messages + + prompt = _('To add to your post EDIT ABOVE THIS LINE') + reply_separator_line = const.SIMPLE_REPLY_SEPARATOR_TEMPLATE % prompt + data = { + 'site_name': askbot_settings.APP_SHORT_NAME, + 'post': revision.post, + 'replace_content_address': replace_content_address, + 'reply_separator_line': reply_separator_line, + 'mailto_link_subject': mailto_link_subject, + 'reply_code': reply_code + } + + #load the template + from askbot.skins.loaders import get_template + template = get_template('email/notify_author_about_approved_post.html') + #todo: possibly add headers to organize messages in threads + headers = {'Reply-To': append_content_address} + #send the message + mail.send_mail( + subject_line = _('Your post at %(site_name)s was approved') % data, + body_text = template.render(Context(data)), + recipient_list = [revision.author.email,], + related_object = revision, + activity_type = const.TYPE_ACTIVITY_EMAIL_UPDATE_SENT, + headers = headers + ) @task(ignore_result = True) def record_post_update_celery_task( diff --git a/askbot/tests/email_alert_tests.py b/askbot/tests/email_alert_tests.py index 5f20496d..6d0cc105 100644 --- a/askbot/tests/email_alert_tests.py +++ b/askbot/tests/email_alert_tests.py @@ -954,26 +954,34 @@ class PostApprovalTests(utils.AskbotTestCase): """test notifications sent to authors when their posts are approved or published""" def setUp(self): + self.reply_by_email = askbot_settings.REPLY_BY_EMAIL + askbot_settings.update('REPLY_BY_EMAIL', True) + self.enable_content_moderation = \ + askbot_settings.ENABLE_CONTENT_MODERATION + askbot_settings.update('ENABLE_CONTENT_MODERATION', True) assert( django_settings.EMAIL_BACKEND == 'django.core.mail.backends.locmem.EmailBackend' ) + def tearDown(self): + askbot_settings.update( + 'REPLY_BY_EMAIL', self.reply_by_email + ) + askbot_settings.update( + 'ENABLE_CONTENT_MODERATION', + self.enable_content_moderation + ) + def test_emailed_question_answerable_approval_notification(self): - setting_backup = askbot_settings.REPLY_BY_EMAIL - askbot_settings.update('REPLY_BY_EMAIL', True) - self.u1 = self.create_user('user1', status = 'a') + self.u1 = self.create_user('user1', status = 'a')#regular user question = self.post_question(user = self.u1, by_email = True) outbox = django.core.mail.outbox + #here we should get just the notification of the post + #being placed on the moderation queue self.assertEquals(len(outbox), 1) self.assertEquals(outbox[0].recipients(), [self.u1.email]) - askbot_settings.update('REPLY_BY_EMAIL', setting_backup) def test_moderated_question_answerable_approval_notification(self): - setting_backup1 = askbot_settings.REPLY_BY_EMAIL - askbot_settings.update('REPLY_BY_EMAIL', True) - setting_backup2 = askbot_settings.ENABLE_CONTENT_MODERATION - askbot_settings.update('ENABLE_CONTENT_MODERATION', True) - u1 = self.create_user('user1', status = 'a') question = self.post_question(user = u1, by_email = True) @@ -989,7 +997,3 @@ class PostApprovalTests(utils.AskbotTestCase): #moderation notification self.assertEquals(outbox[0].recipients(), [u1.email,]) self.assertEquals(outbox[1].recipients(), [u1.email,])#approval - - askbot_settings.update('REPLY_BY_EMAIL', setting_backup1) - askbot_settings.update('ENABLE_CONTENT_MODERATION', setting_backup2) - diff --git a/askbot/tests/reply_by_email_tests.py b/askbot/tests/reply_by_email_tests.py index b5132dcf..177deebd 100644 --- a/askbot/tests/reply_by_email_tests.py +++ b/askbot/tests/reply_by_email_tests.py @@ -1,6 +1,6 @@ from django.utils.translation import ugettext as _ from askbot.models import ReplyAddress -from askbot.lamson_handlers import PROCESS, VALIDATE_EMAIL, get_parts +from askbot.mail.lamson_handlers import PROCESS, VALIDATE_EMAIL, get_parts from askbot import const -- cgit v1.2.3-1-g7c22 From 78ba55f5f46705054c6b2b11a9939b89bb807684 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Mon, 28 May 2012 08:03:21 -0400 Subject: more bug fixes in the "ask/answer/comment/edit" by email --- askbot/mail/lamson_handlers.py | 18 +++++++++++++++--- askbot/models/__init__.py | 2 +- .../email/notify_author_about_approved_post.html | 7 ++++--- askbot/tasks.py | 2 ++ 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/askbot/mail/lamson_handlers.py b/askbot/mail/lamson_handlers.py index afcbb031..e98e3e99 100644 --- a/askbot/mail/lamson_handlers.py +++ b/askbot/mail/lamson_handlers.py @@ -268,13 +268,25 @@ def PROCESS( reply_code = reply_address_object.address body_text, stored_files, signature = mail.process_parts(parts, reply_code) - #update signature and validate email address + #if have signature update signature user = reply_address_object.user - if signature and signature != user.email_signature: - user.email_signature = signature + if signature:#if there, then it was stripped + if signature != user.email_signature: + user.email_signature = signature + else:#try to strip signature + stripped_body_text = user.strip_email_signature(body_text) + #todo: add test cases for emails without the signature + if stripped_body_text == body_text and user.email_signature: + #todo: send an email asking to update the signature + raise ValueError('email signature changed or unknown') + body_text = stripped_body_text + + #validate email address user.email_isvalid = True user.save()#todo: actually, saving is not necessary, if nothing changed + #todo: elaborate - here in some cases we want to add an edit + #and in other cases - replace the text entirely if reply_address_object.was_used: action = reply_address_object.edit_post else: diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py index 376c7356..701238e1 100644 --- a/askbot/models/__init__.py +++ b/askbot/models/__init__.py @@ -2738,7 +2738,7 @@ def get_reply_to_addresses(user, post): if post.post_type in ('answer', 'comment'): reply_args['reply_action'] = 'post_comment' elif post.post_type == 'question': - reply_args['reply_action'] = 'post_question' + reply_args['reply_action'] = 'post_answer' primary_addr = ReplyAddress.objects.create_new( **reply_args diff --git a/askbot/skins/default/templates/email/notify_author_about_approved_post.html b/askbot/skins/default/templates/email/notify_author_about_approved_post.html index 6083afc1..269d8506 100644 --- a/askbot/skins/default/templates/email/notify_author_about_approved_post.html +++ b/askbot/skins/default/templates/email/notify_author_about_approved_post.html @@ -8,10 +8,11 @@ #} {{ reply_separator_line }}

    {% trans - post_text=post.text|safe_urlquote, - subject=mailto_link_subject|safe_urlquote + post_text = post.text|safe_urlquote, + subject = mailto_link_subject|safe_urlquote, + author_email_signature = author_email_signature|safe_urlquote %}If you would like to edit by email, please -click here{% endtrans %}

    +click here{% endtrans %}

    {% trans %}Below is a copy of your post:{% endtrans %}

    {% if post.post_type == 'question' %}

    {{ post.thread.title }}

    diff --git a/askbot/tasks.py b/askbot/tasks.py index 71719ef3..f12290aa 100644 --- a/askbot/tasks.py +++ b/askbot/tasks.py @@ -37,6 +37,7 @@ from askbot.models.badges import award_badges_signal @task(ignore_result = True) def notify_author_of_published_revision_celery_task(revision): + #todo: move this to ``askbot.mail`` module #for answerable email only for now, because #we don't yet have the template for the read-only notification if askbot_settings.REPLY_BY_EMAIL: @@ -68,6 +69,7 @@ def notify_author_of_published_revision_celery_task(revision): data = { 'site_name': askbot_settings.APP_SHORT_NAME, 'post': revision.post, + 'author_email_signature': revision.author.email_signature, 'replace_content_address': replace_content_address, 'reply_separator_line': reply_separator_line, 'mailto_link_subject': mailto_link_subject, -- cgit v1.2.3-1-g7c22 From 8a1e7f47c1aa74d56e0d40ba46d9c771b6ac33eb Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Tue, 29 May 2012 01:29:43 -0400 Subject: fixes "user profile broken for anon users" with private karma --- askbot/views/users.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/askbot/views/users.py b/askbot/views/users.py index a2146a64..065ca578 100644 --- a/askbot/views/users.py +++ b/askbot/views/users.py @@ -877,8 +877,11 @@ def user(request, id, slug=None, tab_name=None): elif askbot_settings.KARMA_MODE == 'hidden': can_show_karma = False else: - if request.user.is_administrator_or_moderator() \ - or request.user == profile_owner: + if request.user.is_anonymous(): + can_show_karma = False + elif request.user.is_administrator_or_moderator(): + can_show_karma = True + elif request.user == profile_owner: can_show_karma = True else: can_show_karma = False -- cgit v1.2.3-1-g7c22 From d6e30bef98ac85a8606284aba4adaf17de811b63 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Tue, 29 May 2012 03:15:43 -0400 Subject: hopefully it is possible to edit by email --- askbot/mail/lamson_handlers.py | 25 ++++++++++++++--------- askbot/models/__init__.py | 24 +++++++++++----------- askbot/models/reply_by_email.py | 39 +++++++++++++++++++++++++++--------- askbot/tests/reply_by_email_tests.py | 8 ++++---- 4 files changed, 61 insertions(+), 35 deletions(-) diff --git a/askbot/mail/lamson_handlers.py b/askbot/mail/lamson_handlers.py index e98e3e99..d803dcf2 100644 --- a/askbot/mail/lamson_handlers.py +++ b/askbot/mail/lamson_handlers.py @@ -259,16 +259,18 @@ def VALIDATE_EMAIL( def PROCESS( parts = None, reply_address_object = None, + subject_line = None, **kwargs ): """handler to process the emailed message and make a post to askbot based on the contents of the email, including the text body and the file attachments""" - #split email into bits + #1) get actual email content + # todo: factor this out into the process_reply decorator reply_code = reply_address_object.address body_text, stored_files, signature = mail.process_parts(parts, reply_code) - #if have signature update signature + #2) process body text and email signature user = reply_address_object.user if signature:#if there, then it was stripped if signature != user.email_signature: @@ -281,14 +283,17 @@ def PROCESS( raise ValueError('email signature changed or unknown') body_text = stripped_body_text - #validate email address + #3) validate email address and save user user.email_isvalid = True user.save()#todo: actually, saving is not necessary, if nothing changed - #todo: elaborate - here in some cases we want to add an edit - #and in other cases - replace the text entirely - if reply_address_object.was_used: - action = reply_address_object.edit_post - else: - action = reply_address_object.create_reply - action(body_text, stored_files) + #4) actually make an edit in the forum + robj = reply_address_object + add_post_actions = ('post_comment', 'post_answer', 'auto_answer_or_comment') + if robj.reply_action in ('replace_content', 'append_content'): + robj.edit_post(body_text, title = subject_line) + elif robj.reply_action in add_post_actions: + if robj.was_used: + robj.edit_post(body_text, reply_action = 'append_content') + else: + robj.create_reply(body_text) diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py index 701238e1..2526c4b7 100644 --- a/askbot/models/__init__.py +++ b/askbot/models/__init__.py @@ -1503,18 +1503,18 @@ def user_edit_post(self, @auto_now_timestamp def user_edit_question( - self, - question = None, - title = None, - body_text = None, - revision_comment = None, - tags = None, - wiki = False, - edit_anonymously = False, - timestamp = None, - force = False,#if True - bypass the assert - by_email = False - ): + self, + question = None, + title = None, + body_text = None, + revision_comment = None, + tags = None, + wiki = False, + edit_anonymously = False, + timestamp = None, + force = False,#if True - bypass the assert + by_email = False + ): if force == False: self.assert_can_edit_question(question) diff --git a/askbot/models/reply_by_email.py b/askbot/models/reply_by_email.py index 4981765f..bcd82645 100644 --- a/askbot/models/reply_by_email.py +++ b/askbot/models/reply_by_email.py @@ -36,6 +36,8 @@ class ReplyAddressManager(BaseQuerySetManager): REPLY_ACTION_CHOICES = ( ('post_answer', _('Post an answer')), ('post_comment', _('Post a comment')), + ('replace_content', _('Edit post')), + ('append_content', _('Append to post')), ('auto_answer_or_comment', _('Answer or comment, depending on the size of post')), ('validate_email', _('Validate email and record signature')), ) @@ -83,22 +85,41 @@ class ReplyAddress(models.Model): askbot_settings.REPLY_BY_EMAIL_HOSTNAME ) - def edit_post(self, body_text, stored_files): + def edit_post( + self, body_text, title = None, reply_action = None + ): """edits the created post upon repeated response to the same address""" - assert self.was_used == True - self.user.edit_post( - post = self.response_post, - body_text = stored_files, - revision_comment = _('edited by email'), - by_email = True - ) + reply_action = reply_action or self.reply_action + + if reply_action == 'append_content': + body_text = self.post.text + '\n\n' + body_text + revision_comment = _('added content by email') + else: + revision_comment = _('edited by email') + + if self.post.post_type == 'question': + self.user.edit_question( + question = self.post, + body_text = body_text, + title = title, + revision_comment = revision_comment, + by_email = True + ) + else: + self.user.edit_post( + post = self.response_post, + body_text = body_text, + revision_comment = revision_comment, + by_email = True + ) self.response_post.thread.invalidate_cached_data() - def create_reply(self, body_text, stored_files): + def create_reply(self, body_text): """creates a reply to the post which was emailed to the user """ + assert(self.was_used == False) result = None if self.post.post_type == 'answer': result = self.user.post_comment( diff --git a/askbot/tests/reply_by_email_tests.py b/askbot/tests/reply_by_email_tests.py index 177deebd..698662fc 100644 --- a/askbot/tests/reply_by_email_tests.py +++ b/askbot/tests/reply_by_email_tests.py @@ -101,7 +101,7 @@ class ReplyAddressModelTests(AskbotTestCase): post = self.answer, user = self.u1 ) - post = result.create_reply(TEST_CONTENT, []) + post = result.create_reply(TEST_CONTENT) self.assertEquals(post.post_type, "comment") self.assertEquals(post.text, TEST_CONTENT) self.assertEquals(self.answer.comments.count(), 2) @@ -111,7 +111,7 @@ class ReplyAddressModelTests(AskbotTestCase): post = self.comment, user = self.u1 ) - post = result.create_reply(TEST_CONTENT, []) + post = result.create_reply(TEST_CONTENT) self.assertEquals(post.post_type, "comment") self.assertEquals(post.text, TEST_CONTENT) self.assertEquals(self.answer.comments.count(), 2) @@ -122,7 +122,7 @@ class ReplyAddressModelTests(AskbotTestCase): post = self.question, user = self.u3 ) - post = result.create_reply(TEST_CONTENT, []) + post = result.create_reply(TEST_CONTENT) self.assertEquals(post.post_type, "comment") self.assertEquals(post.text, TEST_CONTENT) @@ -131,7 +131,7 @@ class ReplyAddressModelTests(AskbotTestCase): post = self.question, user = self.u3 ) - post = result.create_reply(TEST_LONG_CONTENT, []) + post = result.create_reply(TEST_LONG_CONTENT) self.assertEquals(post.post_type, "answer") self.assertEquals(post.text, TEST_LONG_CONTENT) -- cgit v1.2.3-1-g7c22 From acab77ccc328a869a6f106fe91c969c17efee0cf Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Tue, 29 May 2012 03:48:50 -0400 Subject: change ugettext_lazy to ugettext on value going to db --- askbot/models/reply_by_email.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/askbot/models/reply_by_email.py b/askbot/models/reply_by_email.py index bcd82645..8cb88241 100644 --- a/askbot/models/reply_by_email.py +++ b/askbot/models/reply_by_email.py @@ -4,7 +4,7 @@ import string import logging from django.db import models from django.contrib.auth.models import User -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ugettext as _ from askbot.models.post import Post from askbot.models.base import BaseQuerySetManager from askbot.conf import settings as askbot_settings -- cgit v1.2.3-1-g7c22 From d7cd45432b5773717cb416fab2e8207b569e6de5 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Tue, 29 May 2012 03:53:19 -0400 Subject: omitted editing title from "append" content type emails on questions --- askbot/mail/lamson_handlers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/askbot/mail/lamson_handlers.py b/askbot/mail/lamson_handlers.py index d803dcf2..7ecdb0a9 100644 --- a/askbot/mail/lamson_handlers.py +++ b/askbot/mail/lamson_handlers.py @@ -290,8 +290,10 @@ def PROCESS( #4) actually make an edit in the forum robj = reply_address_object add_post_actions = ('post_comment', 'post_answer', 'auto_answer_or_comment') - if robj.reply_action in ('replace_content', 'append_content'): + if robj.reply_action == 'replace_content': robj.edit_post(body_text, title = subject_line) + elif robj.reply_action == 'append_content': + robj.edit_post(body_text)#in this case we don't touch the title elif robj.reply_action in add_post_actions: if robj.was_used: robj.edit_post(body_text, reply_action = 'append_content') -- cgit v1.2.3-1-g7c22 From 265e91eed1f01e435d7804f82bbe9c8a1e394687 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Tue, 29 May 2012 05:23:43 -0400 Subject: in the user inbox skip notifications about deleted comments to prevent an exception, and a fix in editing of posts by email --- askbot/models/__init__.py | 5 +++++ askbot/models/reply_by_email.py | 7 ++++++- askbot/views/users.py | 4 ++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py index 2526c4b7..30df6366 100644 --- a/askbot/models/__init__.py +++ b/askbot/models/__init__.py @@ -1252,6 +1252,11 @@ def user_delete_comment( timestamp = None ): self.assert_can_delete_comment(comment = comment) + #todo: we want to do this + #comment.deleted = True + #comment.deleted_by = self + #comment.deleted_at = timestamp + #comment.save() comment.delete() comment.thread.invalidate_cached_data() diff --git a/askbot/models/reply_by_email.py b/askbot/models/reply_by_email.py index 8cb88241..0bf46a14 100644 --- a/askbot/models/reply_by_email.py +++ b/askbot/models/reply_by_email.py @@ -113,7 +113,12 @@ class ReplyAddress(models.Model): revision_comment = revision_comment, by_email = True ) - self.response_post.thread.invalidate_cached_data() + #todo: why do we have these branches? + if self.response_post: + thread = self.response_post.thread + else: + thread = self.post.thread + thread.invalidate_cached_data() def create_reply(self, body_text): """creates a reply to the post which was emailed diff --git a/askbot/views/users.py b/askbot/views/users.py index 065ca578..4d425b60 100644 --- a/askbot/views/users.py +++ b/askbot/views/users.py @@ -662,6 +662,10 @@ def user_responses(request, user, context): response_list = list() for memo in memo_set: #a monster query chain below + if memo.activity is None: + #todo: this is a temporary plug, due to + #poor handling of comment deletion - see User.delete_comment() + continue response = { 'id': memo.id, 'timestamp': memo.activity.active_at, -- cgit v1.2.3-1-g7c22 From 3daa8afeec131a482b16ec5ddd04b19f837d3e60 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Tue, 29 May 2012 05:32:20 -0400 Subject: fixed the temporary plug in the user inbox to hide activities for deleted objects --- askbot/views/users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/askbot/views/users.py b/askbot/views/users.py index 4d425b60..054e86d2 100644 --- a/askbot/views/users.py +++ b/askbot/views/users.py @@ -662,7 +662,7 @@ def user_responses(request, user, context): response_list = list() for memo in memo_set: #a monster query chain below - if memo.activity is None: + if memo.activity.content_object is None: #todo: this is a temporary plug, due to #poor handling of comment deletion - see User.delete_comment() continue -- cgit v1.2.3-1-g7c22 From a1e7c8aec92894b331c74c18f1e3d4289b176f28 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Tue, 29 May 2012 06:08:28 -0400 Subject: fixed an error in the generator of response email addresses --- askbot/models/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py index 30df6366..88041acc 100644 --- a/askbot/models/__init__.py +++ b/askbot/models/__init__.py @@ -2732,7 +2732,7 @@ def get_reply_to_addresses(user, post): #these variables will contain return values primary_addr = django_settings.DEFAULT_FROM_EMAIL secondary_addr = None - if askbot_settings.REPLY_BY_EMAIL: + if user.can_reply_by_email(): if user.reputation >= askbot_settings.MIN_REP_TO_POST_BY_EMAIL: reply_args = { -- cgit v1.2.3-1-g7c22 From 75063494b765e16c571cf623d3bda251532b3429 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Tue, 29 May 2012 06:25:05 -0400 Subject: a bug fix in the edit post branch by email --- askbot/models/reply_by_email.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/askbot/models/reply_by_email.py b/askbot/models/reply_by_email.py index 0bf46a14..166c1dd6 100644 --- a/askbot/models/reply_by_email.py +++ b/askbot/models/reply_by_email.py @@ -106,19 +106,16 @@ class ReplyAddress(models.Model): revision_comment = revision_comment, by_email = True ) + post = self.post else: + post = self.response_post or self.post self.user.edit_post( - post = self.response_post, + post = post, body_text = body_text, revision_comment = revision_comment, by_email = True ) - #todo: why do we have these branches? - if self.response_post: - thread = self.response_post.thread - else: - thread = self.post.thread - thread.invalidate_cached_data() + post.thread.invalidate_cached_data() def create_reply(self, body_text): """creates a reply to the post which was emailed -- cgit v1.2.3-1-g7c22 From 6cc252e045d20f301d61c04bcf134fa5988dac67 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Tue, 29 May 2012 07:37:10 -0400 Subject: another bug fix to process post edits by email correctly --- askbot/mail/lamson_handlers.py | 2 +- askbot/models/reply_by_email.py | 27 +++++++++++++++++---------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/askbot/mail/lamson_handlers.py b/askbot/mail/lamson_handlers.py index 7ecdb0a9..6f94c990 100644 --- a/askbot/mail/lamson_handlers.py +++ b/askbot/mail/lamson_handlers.py @@ -296,6 +296,6 @@ def PROCESS( robj.edit_post(body_text)#in this case we don't touch the title elif robj.reply_action in add_post_actions: if robj.was_used: - robj.edit_post(body_text, reply_action = 'append_content') + robj.edit_post(body_text, edit_response = True) else: robj.create_reply(body_text) diff --git a/askbot/models/reply_by_email.py b/askbot/models/reply_by_email.py index 166c1dd6..ac96fc58 100644 --- a/askbot/models/reply_by_email.py +++ b/askbot/models/reply_by_email.py @@ -86,42 +86,49 @@ class ReplyAddress(models.Model): ) def edit_post( - self, body_text, title = None, reply_action = None + self, body_text, title = None, edit_response = False ): """edits the created post upon repeated response to the same address""" - reply_action = reply_action or self.reply_action + if self.was_used or edit_response: + reply_action = 'append_content' + else: + reply_action = self.reply_action - if reply_action == 'append_content': - body_text = self.post.text + '\n\n' + body_text + if edit_response: + post = self.response_post + else: + post = self.post + + if self.reply_action == 'append_content': + body_text = post.text + '\n\n' + body_text revision_comment = _('added content by email') else: + assert(reply_action == 'replace_content') revision_comment = _('edited by email') - if self.post.post_type == 'question': + if post.post_type == 'question': + assert(post is self.post) self.user.edit_question( - question = self.post, + question = post, body_text = body_text, title = title, revision_comment = revision_comment, by_email = True ) - post = self.post else: - post = self.response_post or self.post self.user.edit_post( post = post, body_text = body_text, revision_comment = revision_comment, by_email = True ) - post.thread.invalidate_cached_data() + self.post.thread.invalidate_cached_data() def create_reply(self, body_text): """creates a reply to the post which was emailed to the user """ - assert(self.was_used == False) result = None if self.post.post_type == 'answer': result = self.user.post_comment( -- cgit v1.2.3-1-g7c22 From 56bd41f8cecc4878a26859dc7663bbb1764ee35e Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Tue, 29 May 2012 11:38:36 -0400 Subject: a workaround to contextless activity issue in inbox --- askbot/views/users.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/askbot/views/users.py b/askbot/views/users.py index 0997f21e..19613838 100644 --- a/askbot/views/users.py +++ b/askbot/views/users.py @@ -655,7 +655,8 @@ def user_responses(request, user, context): #3) "package" data for the output response_list = list() for memo in memo_set: - #a monster query chain below + if memo.activity.content_object is None: + continue#a temp plug due to bug in the comment deletion response = { 'id': memo.id, 'timestamp': memo.activity.active_at, -- cgit v1.2.3-1-g7c22 From 1c1ecce363dbeb3e4ccca228a7af115ae1238ff2 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Tue, 29 May 2012 11:53:31 -0400 Subject: stripped whitespace from post body --- askbot/mail/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/askbot/mail/__init__.py b/askbot/mail/__init__.py index 35f3ac62..153bbd56 100644 --- a/askbot/mail/__init__.py +++ b/askbot/mail/__init__.py @@ -288,7 +288,7 @@ def process_parts(parts, reply_code = None): stored_files.append(stored_file) attachments_markdown += '\n\n' + markdown elif part_type == 'body': - body_markdown += '\n\n' + content + body_markdown += '\n\n' + content.strip('\n\t ') elif part_type == 'inline': markdown, stored_file = process_attachment(content) stored_files.append(stored_file) -- cgit v1.2.3-1-g7c22 From a34d5ef02c4d0f6403aa4c6238b9ea0f36290f13 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Tue, 29 May 2012 11:59:54 -0400 Subject: a bug fix in replying by email --- askbot/mail/lamson_handlers.py | 3 ++- askbot/models/reply_by_email.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/askbot/mail/lamson_handlers.py b/askbot/mail/lamson_handlers.py index 6f94c990..467493c7 100644 --- a/askbot/mail/lamson_handlers.py +++ b/askbot/mail/lamson_handlers.py @@ -187,7 +187,8 @@ def ASK(message, host = None, addr = None): parts = get_parts(message) from_address = message.From - subject = message['Subject']#why lamson does not give it normally? + #why lamson does not give it normally? + subject = message['Subject'].strip('\n\t ') body_text, stored_files, unused = mail.process_parts(parts) if addr == 'ask': mail.process_emailed_question( diff --git a/askbot/models/reply_by_email.py b/askbot/models/reply_by_email.py index ac96fc58..0db7244c 100644 --- a/askbot/models/reply_by_email.py +++ b/askbot/models/reply_by_email.py @@ -100,7 +100,7 @@ class ReplyAddress(models.Model): else: post = self.post - if self.reply_action == 'append_content': + if reply_action == 'append_content': body_text = post.text + '\n\n' + body_text revision_comment = _('added content by email') else: -- cgit v1.2.3-1-g7c22 From 804b84e448d43f3e2adc9d4bbc21c081d468bb49 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Wed, 30 May 2012 11:29:45 -0400 Subject: another plug for the deleted comments --- askbot/views/users.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/askbot/views/users.py b/askbot/views/users.py index 19613838..e38b7f50 100644 --- a/askbot/views/users.py +++ b/askbot/views/users.py @@ -585,12 +585,13 @@ def user_recent(request, user, context): elif activity.activity_type == const.TYPE_ACTIVITY_PRIZE: award = activity.content_object - activities.append(AwardEvent( - time=award.awarded_at, - type=activity.activity_type, - content_object=award.content_object, - badge=award.badge, - )) + if award is not None:#todo: work around halfa$$ comment deletion + activities.append(AwardEvent( + time=award.awarded_at, + type=activity.activity_type, + content_object=award.content_object, + badge=award.badge, + )) activities.sort(key=operator.attrgetter('time'), reverse=True) -- cgit v1.2.3-1-g7c22 From 226e869cc2c85495bd29e7b8fb74a71e830e4003 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Wed, 30 May 2012 11:37:26 -0400 Subject: fixed mailto link and replaced quote_plus with quote fo encode content and subject for the mailto links --- askbot/const/__init__.py | 2 +- .../default/templates/email/notify_author_about_approved_post.html | 2 +- askbot/templatetags/extra_filters_jinja.py | 7 +++++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/askbot/const/__init__.py b/askbot/const/__init__.py index e711de92..0a36879e 100644 --- a/askbot/const/__init__.py +++ b/askbot/const/__init__.py @@ -58,7 +58,7 @@ SIMPLE_REPLY_SEPARATOR_TEMPLATE = '==== %s -=-==' REPLY_SEPARATOR_TEMPLATE = '==== %(user_action)s %(instruction)s -=-==' REPLY_WITH_COMMENT_TEMPLATE = _( 'Note: to reply with a comment, ' - 'please use this link' + 'please use this link' ) REPLY_SEPARATOR_REGEX = re.compile(r'==== .* -=-==', re.MULTILINE|re.DOTALL) diff --git a/askbot/skins/default/templates/email/notify_author_about_approved_post.html b/askbot/skins/default/templates/email/notify_author_about_approved_post.html index 269d8506..085141d9 100644 --- a/askbot/skins/default/templates/email/notify_author_about_approved_post.html +++ b/askbot/skins/default/templates/email/notify_author_about_approved_post.html @@ -12,7 +12,7 @@ subject = mailto_link_subject|safe_urlquote, author_email_signature = author_email_signature|safe_urlquote %}If you would like to edit by email, please -click here{% endtrans %}

    +click here{% endtrans %}

    {% trans %}Below is a copy of your post:{% endtrans %}

    {% if post.post_type == 'question' %}

    {{ post.thread.title }}

    diff --git a/askbot/templatetags/extra_filters_jinja.py b/askbot/templatetags/extra_filters_jinja.py index fa9d0ced..3643e3c9 100644 --- a/askbot/templatetags/extra_filters_jinja.py +++ b/askbot/templatetags/extra_filters_jinja.py @@ -47,8 +47,11 @@ def add_tz_offset(datetime_object): return str(datetime_object) + ' ' + TIMEZONE_STR @register.filter -def safe_urlquote(text): - return urllib.quote_plus(text.encode('utf8')) +def safe_urlquote(text, quote_plus = False): + if quote_plus: + return urllib.quote_plus(text.encode('utf8')) + else: + return urllib.quote(text.encode('utf8')) @register.filter def strip_path(url): -- cgit v1.2.3-1-g7c22 From 393b50cd9f4036c7f9b17ef10f82f740cf3ce996 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Wed, 30 May 2012 11:48:56 -0400 Subject: fixed trailing quote stripping in the response processing --- askbot/mail/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/askbot/mail/__init__.py b/askbot/mail/__init__.py index 153bbd56..a0086a0a 100644 --- a/askbot/mail/__init__.py +++ b/askbot/mail/__init__.py @@ -264,7 +264,7 @@ def extract_user_signature(text, reply_code): #strip off the leading quoted lines, there could be one or two #also strip empty lines - while tail[0].startswith('>') or tail[0].strip() == '': + while tail and (tail[0].startswith('>') or tail[0].strip() == ''): tail.pop(0) return '\n'.join(tail) -- cgit v1.2.3-1-g7c22 From 4aacfd1cd9dc827e8259daa622e1e2cc872e0162 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Wed, 30 May 2012 12:00:51 -0400 Subject: hopefully fixed the repeated signature issue for some email clients --- askbot/models/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py index 88041acc..fadbe450 100644 --- a/askbot/models/__init__.py +++ b/askbot/models/__init__.py @@ -207,12 +207,12 @@ def user_update_avatar_type(self): def user_strip_email_signature(self, text): """strips email signature from the end of the text""" - if self.email_signature == '': + if self.email_signature.strip() == '': return text text = '\n'.join(text.splitlines())#normalize the line endings - if text.endswith(self.email_signature): - return text[0:-len(self.email_signature)] + while text.endswith(self.email_signature): + text = text[0:-len(self.email_signature)] return text def _check_gravatar(gravatar): -- cgit v1.2.3-1-g7c22 From ad74fdd061fcf4811d90458c2e29bbe124d3e12e Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Wed, 30 May 2012 16:06:08 -0400 Subject: added subject to the post with comment mailto link --- askbot/const/__init__.py | 2 +- askbot/models/__init__.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/askbot/const/__init__.py b/askbot/const/__init__.py index 0a36879e..8e7ba9e6 100644 --- a/askbot/const/__init__.py +++ b/askbot/const/__init__.py @@ -58,7 +58,7 @@ SIMPLE_REPLY_SEPARATOR_TEMPLATE = '==== %s -=-==' REPLY_SEPARATOR_TEMPLATE = '==== %(user_action)s %(instruction)s -=-==' REPLY_WITH_COMMENT_TEMPLATE = _( 'Note: to reply with a comment, ' - 'please use this link' + 'please use this link' ) REPLY_SEPARATOR_REGEX = re.compile(r'==== .* -=-==', re.MULTILINE|re.DOTALL) diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py index fadbe450..1583b2e5 100644 --- a/askbot/models/__init__.py +++ b/askbot/models/__init__.py @@ -2685,7 +2685,12 @@ def format_instant_notification_email( 'instruction': _('To reply, PLEASE WRITE ABOVE THIS LINE.') } if post.post_type == 'question' and alt_reply_address: - data = {'addr': alt_reply_address} + data = { + 'addr': alt_reply_address, + 'subject': urllib.quote( + ('Re: ' + post.thread.title).encode('utf-8') + ) + } reply_separator += '
    ' + \ const.REPLY_WITH_COMMENT_TEMPLATE % data else: -- cgit v1.2.3-1-g7c22 From eff58f74b06e581c787500c7303f26cccce565f2 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Wed, 30 May 2012 22:17:55 -0400 Subject: added setting allowing to change when author of the emailed post is notified of his own posting --- askbot/conf/email.py | 30 +++++++++++++++++++++++++----- askbot/const/__init__.py | 39 +++++++++++++++++++++++++++++++++++++++ askbot/models/__init__.py | 14 +++++++++++--- askbot/models/post.py | 35 ++++++++++++++++++++++++++--------- askbot/models/signals.py | 7 ++++++- askbot/tests/email_alert_tests.py | 8 ++++++++ 6 files changed, 115 insertions(+), 18 deletions(-) diff --git a/askbot/conf/email.py b/askbot/conf/email.py index 195f36e3..1b81fa96 100644 --- a/askbot/conf/email.py +++ b/askbot/conf/email.py @@ -274,8 +274,6 @@ settings.register( ) ) - - settings.register( livesettings.BooleanValue( EMAIL, @@ -290,6 +288,31 @@ settings.register( ) ) +settings.register( + livesettings.StringValue( + EMAIL, + 'SELF_NOTIFY_EMAILED_POST_AUTHOR_WHEN', + description = _( + 'Emailed post: when to notify author about publishing' + ), + choices = const.SELF_NOTIFY_EMAILED_POST_AUTHOR_WHEN_CHOICES, + default = const.NEVER + ) +) + +#not implemented at this point +#settings.register( +# livesettings.IntegerValue( +# EMAIL, +# 'SELF_NOTIFY_WEB_POST_AUTHOR_WHEN', +# description = _( +# 'Web post: when to notify author about publishing' +# ), +# choices = const.SELF_NOTIFY_WEB_POST_AUTHOR_WHEN_CHOICES, +# default = const.NEVER +# ) +#) + settings.register( livesettings.StringValue( EMAIL, @@ -301,8 +324,6 @@ settings.register( ) ) - - settings.register( livesettings.IntegerValue( EMAIL, @@ -311,4 +332,3 @@ settings.register( description=_('Email replies having fewer words than this number will be posted as comments instead of answers') ) ) - diff --git a/askbot/const/__init__.py b/askbot/const/__init__.py index 8e7ba9e6..e693a63c 100644 --- a/askbot/const/__init__.py +++ b/askbot/const/__init__.py @@ -6,6 +6,7 @@ text in this project, all unicode text go here. """ from django.utils.translation import ugettext as _ import re + CLOSE_REASONS = ( (1, _('duplicate question')), (2, _('question is off-topic or not relevant')), @@ -55,6 +56,44 @@ POST_SORT_METHODS = ( POST_TYPES = ('answer', 'comment', 'question', 'tag_wiki', 'reject_reason') SIMPLE_REPLY_SEPARATOR_TEMPLATE = '==== %s -=-==' + +#values for SELF_NOTIFY_WHEN... settings use bits +NEVER = 'never' +FOR_FIRST_REVISION = 'first' +FOR_ANY_REVISION = 'any' +SELF_NOTIFY_EMAILED_POST_AUTHOR_WHEN_CHOICES = ( + (NEVER, _('Never')), + (FOR_FIRST_REVISION, _('When new post is published')), + (FOR_ANY_REVISION, _('When post is published or revised')), +) +#need more options for web posts b/c user is looking at the page +#when posting. when posts are made by email - user is not looking +#at the site and therefore won't get any feedback unless an email is sent back +#todo: rename INITIAL -> FIRST and make values of type string +#FOR_INITIAL_REVISION_WHEN_APPROVED = 1 +#FOR_ANY_REVISION_WHEN_APPROVED = 2 +#FOR_INITIAL_REVISION_ALWAYS = 3 +#FOR_ANY_REVISION_ALWAYS = 4 +#SELF_NOTIFY_WEB_POST_AUTHOR_WHEN_CHOICES = ( +# (NEVER, _('Never')), +# ( +# FOR_INITIAL_REVISION_WHEN_APPROVED, +# _('When inital revision is approved by moderator') +# ), +# ( +# FOR_ANY_REVISION_WHEN_APPROVED, +# _('When any revision is approved by moderator') +# ), +# ( +# FOR_INITIAL_REVISION_ALWAYS, +# _('Any time when inital revision is published') +# ), +# ( +# FOR_ANY_REVISION_ALWAYS, +# _('Any time when revision is published') +# ) +#) + REPLY_SEPARATOR_TEMPLATE = '==== %(user_action)s %(instruction)s -=-==' REPLY_WITH_COMMENT_TEMPLATE = _( 'Note: to reply with a comment, ' diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py index 1583b2e5..8c96ce8c 100644 --- a/askbot/models/__init__.py +++ b/askbot/models/__init__.py @@ -2299,7 +2299,9 @@ def user_approve_post_revision(user, post_revision, timestamp = None): post.thread.invalidate_cached_data() #send the signal of published revision - signals.post_revision_published.send(None, revision = post_revision) + signals.post_revision_published.send( + None, revision = post_revision, was_approved = True + ) @auto_now_timestamp def flag_post(user, post, timestamp=None, cancel=False, cancel_all = False, force = False): @@ -2817,11 +2819,14 @@ def send_instant_notifications_about_activity_in_post( headers = headers ) -def notify_author_of_published_revision(revision, **kwargs): +def notify_author_of_published_revision( + revision = None, was_approved = None, **kwargs +): """notifies author about approved post revision, assumes that we have the very first revision """ - if revision.revision == 1:#only email about first revision + #only email about first revision + if revision.should_notify_author_about_publishing(was_approved): from askbot.tasks import notify_author_of_published_revision_celery_task notify_author_of_published_revision_celery_task.delay(revision) @@ -3243,6 +3248,9 @@ signals.user_updated.connect(record_user_full_updated, sender=User) signals.user_logged_in.connect(complete_pending_tag_subscriptions)#todo: add this to fake onlogin middleware signals.user_logged_in.connect(post_anonymous_askbot_content) signals.post_updated.connect(record_post_update_activity) + +#probably we cannot use post-save here the point of this is +#to tell when the revision becomes publicly visible, not when it is saved signals.post_revision_published.connect(notify_author_of_published_revision) signals.site_visited.connect(record_user_visit) diff --git a/askbot/models/post.py b/askbot/models/post.py index 0bc57ebb..06db7688 100644 --- a/askbot/models/post.py +++ b/askbot/models/post.py @@ -345,7 +345,7 @@ class Post(models.Model): db_table = 'askbot_post' - def parse_post_text(post): + def parse_post_text(self): """typically post has a field to store raw source text in comment it is called .comment, in Question and Answer it is called .text @@ -361,18 +361,18 @@ class Post(models.Model): removed_mentions - list of mention objects - for removed ones """ - if post.post_type in ('question', 'answer', 'tag_wiki', 'reject_reason'): + if self.post_type in ('question', 'answer', 'tag_wiki', 'reject_reason'): _urlize = False _use_markdown = True _escape_html = False #markdow does the escaping - elif post.is_comment(): + elif self.is_comment(): _urlize = True _use_markdown = True _escape_html = True else: raise NotImplementedError - text = post.text + text = self.text if _escape_html: text = cgi.escape(text) @@ -384,12 +384,12 @@ class Post(models.Model): text = sanitize_html(markup.get_parser().convert(text)) #todo, add markdown parser call conditional on - #post.use_markdown flag + #self.use_markdown flag post_html = text mentioned_authors = list() removed_mentions = list() if '@' in text: - op = post.get_origin_post() + op = self.get_origin_post() anticipated_authors = op.get_author_list( include_comments = True, recursive = True @@ -416,10 +416,10 @@ class Post(models.Model): #find mentions that were removed and identify any previously #entered mentions so that we can send alerts on only new ones from askbot.models.user import Activity - if post.pk is not None: + if self.pk is not None: #only look for previous mentions if post was already saved before prev_mention_qs = Activity.objects.get_mentions( - mentioned_in = post + mentioned_in = self ) new_set = set(mentioned_authors) for prev_mention in prev_mention_qs: @@ -1778,6 +1778,7 @@ class PostRevision(models.Model): self.post.thread.save() #above changes will hide post from the public display if self.by_email: + #todo: move this to the askbot.mail module from askbot.mail import send_mail email_context = { 'site': askbot_settings.APP_SHORT_NAME @@ -1818,7 +1819,6 @@ class PostRevision(models.Model): #todo: make this group-sensitive activity.add_recipients(get_admins_and_moderators()) - def moderate_or_publish(self): """either place on moderation queue or announce that this revision is published""" @@ -1828,6 +1828,23 @@ class PostRevision(models.Model): from askbot.models import signals signals.post_revision_published.send(None, revision = self) + def should_notify_author_about_publishing(self, was_approved = False): + """True if author should get email about making own post""" + if self.by_email: + schedule = askbot_settings.SELF_NOTIFY_EMAILED_POST_AUTHOR_WHEN + if schedule == const.NEVER: + return False + elif schedule == const.FOR_FIRST_REVISION: + return self.revision == 1 + elif schedule == const.FOR_ANY_REVISION: + return True + else: + raise ValueError() + else: + #logic not implemented yet + #the ``was_approved`` argument will be used here + #schedule = askbot_settings.SELF_NOTIFY_WEB_POST_AUTHOR_WHEN + return False def revision_type_str(self): return self.REVISION_TYPE_CHOICES_DICT[self.revision_type] diff --git a/askbot/models/signals.py b/askbot/models/signals.py index 1541eff5..d538de76 100644 --- a/askbot/models/signals.py +++ b/askbot/models/signals.py @@ -32,7 +32,12 @@ post_updated = django.dispatch.Signal( 'newly_mentioned_users' ] ) -post_revision_published = django.dispatch.Signal(providing_args = ['revision']) +post_revision_published = django.dispatch.Signal( + providing_args = [ + 'revision', + 'was_approved' + ] + ) site_visited = django.dispatch.Signal(providing_args=['user', 'timestamp']) def pop_signal_receivers(signal): diff --git a/askbot/tests/email_alert_tests.py b/askbot/tests/email_alert_tests.py index 6d0cc105..f07a6613 100644 --- a/askbot/tests/email_alert_tests.py +++ b/askbot/tests/email_alert_tests.py @@ -959,6 +959,10 @@ class PostApprovalTests(utils.AskbotTestCase): self.enable_content_moderation = \ askbot_settings.ENABLE_CONTENT_MODERATION askbot_settings.update('ENABLE_CONTENT_MODERATION', True) + self.self_notify_when = \ + askbot_settings.SELF_NOTIFY_EMAILED_POST_AUTHOR_WHEN + when = const.FOR_FIRST_REVISION + askbot_settings.update('SELF_NOTIFY_EMAILED_POST_AUTHOR_WHEN', when) assert( django_settings.EMAIL_BACKEND == 'django.core.mail.backends.locmem.EmailBackend' ) @@ -971,6 +975,10 @@ class PostApprovalTests(utils.AskbotTestCase): 'ENABLE_CONTENT_MODERATION', self.enable_content_moderation ) + askbot_settings.update( + 'SELF_NOTIFY_EMAILED_POST_AUTHOR_WHEN', + self.self_notify_when + ) def test_emailed_question_answerable_approval_notification(self): self.u1 = self.create_user('user1', status = 'a')#regular user -- cgit v1.2.3-1-g7c22 From 0812271c54dd38a2034bfe42832a5372f28e404d Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Thu, 31 May 2012 00:16:18 -0400 Subject: moved formatting of emailed post contents to template --- askbot/models/post.py | 25 +++++++++-------- .../skins/default/templates/email/quoted_post.html | 31 ++++++++++++++++++++++ askbot/skins/loaders.py | 1 - askbot/tasks.py | 2 +- 4 files changed, 44 insertions(+), 15 deletions(-) create mode 100644 askbot/skins/default/templates/email/quoted_post.html diff --git a/askbot/models/post.py b/askbot/models/post.py index 06db7688..860c63b0 100644 --- a/askbot/models/post.py +++ b/askbot/models/post.py @@ -624,20 +624,19 @@ class Post(models.Model): def format_for_email(self, quote_level = 0): """format post for the output in email, if quote_level > 0, the post will be indented that number of times + todo: move to views? """ - from askbot.templatetags.extra_filters_jinja import absolutize_urls_func - output = '' - if self.post_type == 'question': - output += '%s
    ' % self.thread.title - - output += absolutize_urls_func(self.html) - if self.post_type == 'question':#add tags to the question - output += self.format_tags_for_email() - quote_style = 'padding-left:5px; border-left: 2px solid #aaa;' - while quote_level > 0: - quote_level = quote_level - 1 - output = '
    %s
    ' % (quote_style, output) - return output + from askbot.templatetags.extra_filters_jinja \ + import absolutize_urls_func + from askbot.skins.loaders import get_template + from django.template import Context + template = get_template('email/quoted_post.html') + data = { + 'post': self, + 'quote_level': quote_level, + 'html': absolutize_urls_func(self.html) + } + return template.render(Context(data)) def format_for_email_as_parent_thread_summary(self): """format for email as summary of parent posts diff --git a/askbot/skins/default/templates/email/quoted_post.html b/askbot/skins/default/templates/email/quoted_post.html new file mode 100644 index 00000000..72b2ad52 --- /dev/null +++ b/askbot/skins/default/templates/email/quoted_post.html @@ -0,0 +1,31 @@ +{# Parameters: + * quote_level - integer + * post - the post object +#} +{% spaceless %} + {% for number in range(quote_level) %} +
    + {% endfor %} +

    + {% set author = post.username %} + {% if post.post_type == 'question' %} + {% trans %}Question by {{author}}:{% endtrans %} + {{ post.thread.title }} + {% set tag_names = post.get_tag_names() %} + {% if tag_names %} +

    + {% trans %}Tags:{% endtrans %} + {{ tag_names|join(', ') }}. +

    + {% endif %} + {% elif post.post_type == 'answer' %} + {% trans %}Answer by {{ author }}:{% endtrans %} + {% else %} + {% trans author %}Comment by {{ author }}:{% endtrans %} + {% endif %} +

    + {{ html }} + {% for number in range(quote_level) %} +
    + {% endfor %} +{% endspaceless %} diff --git a/askbot/skins/loaders.py b/askbot/skins/loaders.py index 24559512..aa3188e9 100644 --- a/askbot/skins/loaders.py +++ b/askbot/skins/loaders.py @@ -129,4 +129,3 @@ def render_text_into_skin(text, data, request): skin = get_skin(request) template = skin.from_string(text) return template.render(context) - diff --git a/askbot/tasks.py b/askbot/tasks.py index f12290aa..da07477b 100644 --- a/askbot/tasks.py +++ b/askbot/tasks.py @@ -83,7 +83,7 @@ def notify_author_of_published_revision_celery_task(revision): headers = {'Reply-To': append_content_address} #send the message mail.send_mail( - subject_line = _('Your post at %(site_name)s was approved') % data, + subject_line = _('Your post at %(site_name)s is now published') % data, body_text = template.render(Context(data)), recipient_list = [revision.author.email,], related_object = revision, -- cgit v1.2.3-1-g7c22 From 669d125ebd4eca42151fad5d5f0f1684c86086f6 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Thu, 31 May 2012 02:12:36 -0400 Subject: moved post subthread generation into the template --- askbot/models/post.py | 30 ++----------- askbot/skins/default/templates/email/macros.html | 49 ++++++++++++++++++++++ .../default/templates/email/post_as_subthread.html | 17 ++++++++ .../skins/default/templates/email/quoted_post.html | 33 +-------------- 4 files changed, 72 insertions(+), 57 deletions(-) create mode 100644 askbot/skins/default/templates/email/macros.html create mode 100644 askbot/skins/default/templates/email/post_as_subthread.html diff --git a/askbot/models/post.py b/askbot/models/post.py index 860c63b0..85e25b72 100644 --- a/askbot/models/post.py +++ b/askbot/models/post.py @@ -660,36 +660,14 @@ class Post(models.Model): current_post = parent_post return output - def format_for_email_as_metadata(self): - output = _( - 'Posted by %(user)s on %(date)s' - ) % { - 'user': self.author.username, - 'date': self.added_at.strftime(const.DATETIME_FORMAT) - } - return '

    %s

    ' % output - def format_for_email_as_subthread(self): """outputs question or answer and all it's comments returns empty string for all other post types """ - if self.post_type in ('question', 'answer'): - output = self.format_for_email_as_metadata() - output += self.format_for_email() - comments = self.get_cached_comments() - if comments: - comments_heading = ungettext( - '%(count)d comment:', - '%(count)d comments:', - len(comments) - ) % {'count': len(comments)} - output += '

    %s

    ' % comments_heading - for comment in comments: - output += comment.format_for_email_as_metadata() - output += comment.format_for_email(quote_level = 1) - return output - else: - return '' + from askbot.skins.loaders import get_template + from django.template import Context + template = get_template('email/post_as_subthread.html') + return template.render(Context({'post': self})) def set_cached_comments(self, comments): """caches comments in the lifetime of the object diff --git a/askbot/skins/default/templates/email/macros.html b/askbot/skins/default/templates/email/macros.html new file mode 100644 index 00000000..31c5c54a --- /dev/null +++ b/askbot/skins/default/templates/email/macros.html @@ -0,0 +1,49 @@ +{% macro quoted_post(post = None, quote_level = 0) %} + {% spaceless %} + {{ start_quote(quote_level) }} + {% set author = post.username %} + {% if post.post_type == 'question' %} +

    + {% trans %}Question:{% endtrans %} + {{ post.thread.title }} +

    + {% set tag_names = post.get_tag_names() %} + {% if tag_names %} +

    + {% trans author=post.author.username -%} + Asked by: {{ author }} + {%- endtrans %} +

    +

    + {% trans %}Tags:{% endtrans %} + {{ tag_names|join(', ') }}. +

    + {% endif %} + {% elif post.post_type == 'answer' %} +

    + {% trans %}Answered by {{ author }}:{% endtrans %} +

    + {% else %} +

    + {% trans author %}Commented by {{ author }}:{% endtrans %} +

    + {% endif %} + {{ end_quote(quote_level) }} + {% endspaceless %} +{% endmacro %} + +{% macro start_quote(level = 0) %} + {% for number in range(level) %} +
    + {% endfor %} +{% endmacro %} + +{% macro end_quote(level = 0) %} + {% for number in range(level) %} +
    + {% endfor %} +{% endmacro %} + +{% macro heading_style() %} +font-size:14px;font-weight:bold;margin-bottom:0px; +{% endmacro %} diff --git a/askbot/skins/default/templates/email/post_as_subthread.html b/askbot/skins/default/templates/email/post_as_subthread.html new file mode 100644 index 00000000..9b6eb728 --- /dev/null +++ b/askbot/skins/default/templates/email/post_as_subthread.html @@ -0,0 +1,17 @@ +{% from "email/macros.html" import quoted_post %} +{% if post.post_type in ('question', 'answer') %} + {{ quoted_post(post) }} + {% set comments = post.get_cached_comments() %} + {% if comments %} +

    + {% trans count=comments|length -%} + {{ comment }} comment: + {%- pluralize -%} + {{ count }} comments: + {%- endtrans -%} +

    + {% for comment in comments %} + {{ quoted_post(comment, quote_level = 1) }} + {% endfor %} + {% endif %} +{% endif %} diff --git a/askbot/skins/default/templates/email/quoted_post.html b/askbot/skins/default/templates/email/quoted_post.html index 72b2ad52..d93661a1 100644 --- a/askbot/skins/default/templates/email/quoted_post.html +++ b/askbot/skins/default/templates/email/quoted_post.html @@ -1,31 +1,2 @@ -{# Parameters: - * quote_level - integer - * post - the post object -#} -{% spaceless %} - {% for number in range(quote_level) %} -
    - {% endfor %} -

    - {% set author = post.username %} - {% if post.post_type == 'question' %} - {% trans %}Question by {{author}}:{% endtrans %} - {{ post.thread.title }} - {% set tag_names = post.get_tag_names() %} - {% if tag_names %} -

    - {% trans %}Tags:{% endtrans %} - {{ tag_names|join(', ') }}. -

    - {% endif %} - {% elif post.post_type == 'answer' %} - {% trans %}Answer by {{ author }}:{% endtrans %} - {% else %} - {% trans author %}Comment by {{ author }}:{% endtrans %} - {% endif %} -

    - {{ html }} - {% for number in range(quote_level) %} -
    - {% endfor %} -{% endspaceless %} +{% from "email/macros.html" import quoted_post %} +{{ quoted_post(post, quote_level) }} -- cgit v1.2.3-1-g7c22 From 55df0be6e087338e26e367a7d9894f741348b8d7 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Thu, 31 May 2012 02:23:51 -0400 Subject: an unimporant change --- askbot/models/post.py | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/askbot/models/post.py b/askbot/models/post.py index 85e25b72..81edcc6d 100644 --- a/askbot/models/post.py +++ b/askbot/models/post.py @@ -604,23 +604,6 @@ class Post(models.Model): """ return html_utils.strip_tags(self.html)[:max_length] + ' ...' - def format_tags_for_email(self): - """formats tags of the question post for email""" - tag_style = "white-space: nowrap; " \ - + "font-size: 11px; color: #333;" \ - + "background-color: #EEE;" \ - + "border-left: 3px solid #777;" \ - + "border-top: 1px solid #EEE;" \ - + "border-bottom: 1px solid #CCC;" \ - + "border-right: 1px solid #CCC;" \ - + "padding: 1px 8px 1px 8px;" \ - + "margin-right:3px;" - output = '
    ' - for tag_name in self.get_tag_names(): - output += '%s' % (tag_style, tag_name) - output += '
    ' - return output - def format_for_email(self, quote_level = 0): """format post for the output in email, if quote_level > 0, the post will be indented that number of times @@ -649,13 +632,15 @@ class Post(models.Model): if parent_post is None: break quote_level += 1 + output += '

    ' output += _( - 'In reply to %(user)s %(post)s of %(date)s
    ' + 'In reply to %(user)s %(post)s of %(date)s' ) % { 'user': parent_post.author.username, 'post': _(parent_post.post_type), 'date': parent_post.added_at.strftime(const.DATETIME_FORMAT) } + output += '

    ' output += parent_post.format_for_email(quote_level = quote_level) current_post = parent_post return output -- cgit v1.2.3-1-g7c22 From b5e757b4deaa61f78f9fa3fd9b568ec6f8675e73 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Thu, 31 May 2012 02:36:13 -0400 Subject: added omitted details to the reply email template --- askbot/skins/default/templates/email/macros.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/askbot/skins/default/templates/email/macros.html b/askbot/skins/default/templates/email/macros.html index 31c5c54a..f69dc541 100644 --- a/askbot/skins/default/templates/email/macros.html +++ b/askbot/skins/default/templates/email/macros.html @@ -1,7 +1,7 @@ {% macro quoted_post(post = None, quote_level = 0) %} {% spaceless %} {{ start_quote(quote_level) }} - {% set author = post.username %} + {% set author = post.author.username %} {% if post.post_type == 'question' %}

    {% trans %}Question:{% endtrans %} @@ -10,7 +10,7 @@ {% set tag_names = post.get_tag_names() %} {% if tag_names %}

    - {% trans author=post.author.username -%} + {% trans -%} Asked by: {{ author }} {%- endtrans %}

    @@ -28,6 +28,7 @@ {% trans author %}Commented by {{ author }}:{% endtrans %}

    {% endif %} + {{ post.html }} {{ end_quote(quote_level) }} {% endspaceless %} {% endmacro %} -- cgit v1.2.3-1-g7c22 From 88be32db4e28a546a474523d835d209cb7ec9f13 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Thu, 31 May 2012 03:31:48 -0400 Subject: changed some phrasing in the email notification --- askbot/models/__init__.py | 2 +- askbot/models/post.py | 15 +++++++-- askbot/skins/default/templates/email/macros.html | 38 ++++++++++++++++++++-- .../skins/default/templates/email/quoted_post.html | 5 ++- 4 files changed, 52 insertions(+), 8 deletions(-) diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py index cc1209d2..24d4a3fc 100644 --- a/askbot/models/__init__.py +++ b/askbot/models/__init__.py @@ -2653,7 +2653,7 @@ def format_instant_notification_email( ) #todo: remove hardcoded style else: - content_preview = post.format_for_email() + content_preview = post.format_for_email(is_leaf_post = True) #add indented summaries for the parent posts content_preview += post.format_for_email_as_parent_thread_summary() diff --git a/askbot/models/post.py b/askbot/models/post.py index 81edcc6d..673b4c6f 100644 --- a/askbot/models/post.py +++ b/askbot/models/post.py @@ -604,7 +604,9 @@ class Post(models.Model): """ return html_utils.strip_tags(self.html)[:max_length] + ' ...' - def format_for_email(self, quote_level = 0): + def format_for_email( + self, quote_level = 0, is_leaf_post = False, format = None + ): """format post for the output in email, if quote_level > 0, the post will be indented that number of times todo: move to views? @@ -617,7 +619,9 @@ class Post(models.Model): data = { 'post': self, 'quote_level': quote_level, - 'html': absolutize_urls_func(self.html) + #'html': absolutize_urls_func(self.html), + 'is_leaf_post': is_leaf_post, + 'format': format } return template.render(Context(data)) @@ -632,6 +636,7 @@ class Post(models.Model): if parent_post is None: break quote_level += 1 + """ output += '

    ' output += _( 'In reply to %(user)s %(post)s of %(date)s' @@ -641,7 +646,11 @@ class Post(models.Model): 'date': parent_post.added_at.strftime(const.DATETIME_FORMAT) } output += '

    ' - output += parent_post.format_for_email(quote_level = quote_level) + """ + output += parent_post.format_for_email( + quote_level = quote_level, + format = 'parent_subthread' + ) current_post = parent_post return output diff --git a/askbot/skins/default/templates/email/macros.html b/askbot/skins/default/templates/email/macros.html index f69dc541..078c5c16 100644 --- a/askbot/skins/default/templates/email/macros.html +++ b/askbot/skins/default/templates/email/macros.html @@ -1,4 +1,10 @@ -{% macro quoted_post(post = None, quote_level = 0) %} +{% macro quoted_post( + post = None, + quote_level = 0, + format = None, + is_leaf_post = False + ) +%} {% spaceless %} {{ start_quote(quote_level) }} {% set author = post.author.username %} @@ -21,11 +27,37 @@ {% endif %} {% elif post.post_type == 'answer' %}

    - {% trans %}Answered by {{ author }}:{% endtrans %} + {% if format == 'parent_subthread' %} + {% if is_leaf_post %} + {% trans -%} + {{ author }}'s answer: + {%- endtrans %} + {% else %} + {% trans -%} + In reply to {{ author }}'s answer: + {%- endtrans %} + {% endif %} + {% else %} + {% trans %}Answered by {{ author }}:{% endtrans %} + {% endif %}

    {% else %}

    - {% trans author %}Commented by {{ author }}:{% endtrans %} + {% if format == 'parent_subthread' %} + {% if is_leaf_post %} + {% trans -%} + {{ author }}'s comment: + {%- endtrans %} + {% else %} + {% trans -%} + In reply to {{ author }}'s comment: + {%- endtrans %} + {% endif %} + {% else %} + {% trans author -%} + Commented by {{ author }}: + {%- endtrans %} + {% endif %}

    {% endif %} {{ post.html }} diff --git a/askbot/skins/default/templates/email/quoted_post.html b/askbot/skins/default/templates/email/quoted_post.html index d93661a1..ecc20ad9 100644 --- a/askbot/skins/default/templates/email/quoted_post.html +++ b/askbot/skins/default/templates/email/quoted_post.html @@ -1,2 +1,5 @@ {% from "email/macros.html" import quoted_post %} -{{ quoted_post(post, quote_level) }} +{{ quoted_post( + post, quote_level, is_leaf_post = is_leaf_post, format = format + ) +}} -- cgit v1.2.3-1-g7c22 From e8117ccbe6292ab981bc7fdeaf4271fd74d4ad44 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Thu, 31 May 2012 03:49:03 -0400 Subject: modified format of question heading in the notification --- askbot/skins/default/templates/email/macros.html | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/askbot/skins/default/templates/email/macros.html b/askbot/skins/default/templates/email/macros.html index 078c5c16..c00e8344 100644 --- a/askbot/skins/default/templates/email/macros.html +++ b/askbot/skins/default/templates/email/macros.html @@ -9,8 +9,18 @@ {{ start_quote(quote_level) }} {% set author = post.author.username %} {% if post.post_type == 'question' %} -

    - {% trans %}Question:{% endtrans %} +

    + {% if format == 'parent_subthread' %} + {% if is_leaf_post %} + {% trans -%} + In response to {{ author }}'s question: + {%- endtrans %} + {% else %} + {% trans %}Question:{% endtrans %} + {% endif %} + {% else %} + {% trans %}Question:{% endtrans %} + {% endif %} {{ post.thread.title }}

    {% set tag_names = post.get_tag_names() %} -- cgit v1.2.3-1-g7c22 From 9fcb906ee9ff34f7fb2675f9aa9d3e6252f5b863 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Thu, 31 May 2012 04:01:08 -0400 Subject: another small change in the notification format --- askbot/skins/default/templates/email/macros.html | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/askbot/skins/default/templates/email/macros.html b/askbot/skins/default/templates/email/macros.html index c00e8344..3d86309a 100644 --- a/askbot/skins/default/templates/email/macros.html +++ b/askbot/skins/default/templates/email/macros.html @@ -12,24 +12,24 @@

    {% if format == 'parent_subthread' %} {% if is_leaf_post %} + {% trans %}Question by {{ author }}:{% endtrans %} + {% else %} {% trans -%} In response to {{ author }}'s question: {%- endtrans %} - {% else %} - {% trans %}Question:{% endtrans %} {% endif %} {% else %} - {% trans %}Question:{% endtrans %} + {% trans %}Question :{% endtrans %} {% endif %} {{ post.thread.title }}

    - {% set tag_names = post.get_tag_names() %} - {% if tag_names %}

    - {% trans -%} - Asked by: {{ author }} - {%- endtrans %} + {% if format != 'parent_subthread' %} + {% trans %}Asked by {{ author }}:{% endtrans %} + {% endif %}

    + {% set tag_names = post.get_tag_names() %} + {% if tag_names %}

    {% trans %}Tags:{% endtrans %} {{ tag_names|join(', ') }}. -- cgit v1.2.3-1-g7c22 From 3ed2132bfc00be9769f12d9c4fda64b6b067a731 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Thu, 31 May 2012 04:09:42 -0400 Subject: a one-word edit --- askbot/skins/default/templates/email/macros.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/askbot/skins/default/templates/email/macros.html b/askbot/skins/default/templates/email/macros.html index 3d86309a..577ed753 100644 --- a/askbot/skins/default/templates/email/macros.html +++ b/askbot/skins/default/templates/email/macros.html @@ -15,7 +15,7 @@ {% trans %}Question by {{ author }}:{% endtrans %} {% else %} {% trans -%} - In response to {{ author }}'s question: + In reply to {{ author }}'s question: {%- endtrans %} {% endif %} {% else %} -- cgit v1.2.3-1-g7c22 From 209402f2c5086d95a760fe1007a1f34458badd99 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Thu, 31 May 2012 11:49:50 -0400 Subject: added space around the reply with comment instruction and simplified the separator line in the email alerts --- askbot/models/__init__.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py index 24d4a3fc..3319b9ae 100644 --- a/askbot/models/__init__.py +++ b/askbot/models/__init__.py @@ -2690,10 +2690,8 @@ def format_instant_notification_email( can_reply = to_user.can_reply_by_email() if can_reply: - reply_separator = const.REPLY_SEPARATOR_TEMPLATE % { - 'user_action': user_action, - 'instruction': _('To reply, PLEASE WRITE ABOVE THIS LINE.') - } + reply_separator = const.SIMPLE_REPLY_SEPARATOR_TEMPLATE % \ + _('To reply, PLEASE WRITE ABOVE THIS LINE.') if post.post_type == 'question' and alt_reply_address: data = { 'addr': alt_reply_address, @@ -2701,8 +2699,9 @@ def format_instant_notification_email( ('Re: ' + post.thread.title).encode('utf-8') ) } - reply_separator += '
    ' + \ + reply_separator += '

    ' + \ const.REPLY_WITH_COMMENT_TEMPLATE % data + reply_separator += '

    ' else: reply_separator = user_action -- cgit v1.2.3-1-g7c22 From 995e6714f63f8c11318019a8f63a6fdb2a664a41 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Sat, 2 Jun 2012 21:30:05 -0400 Subject: added optional top banner to the question page --- askbot/conf/sidebar_question.py | 20 +++++++++++++++++--- askbot/doc/source/changelog.rst | 1 + askbot/skins/default/templates/question.html | 3 +++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/askbot/conf/sidebar_question.py b/askbot/conf/sidebar_question.py index 4416823d..bb71be7e 100644 --- a/askbot/conf/sidebar_question.py +++ b/askbot/conf/sidebar_question.py @@ -6,12 +6,26 @@ from askbot.deps.livesettings import ConfigurationGroup from askbot.deps.livesettings import values from django.utils.translation import ugettext as _ from askbot.conf.super_groups import CONTENT_AND_UI -SIDEBAR_QUESTION = ConfigurationGroup( +SIDEBAR_QUESTION = ConfigurationGroup(#shitty name - why sidebar? 'SIDEBAR_QUESTION', - _('Question page sidebar'), + _('Question page banners and sidebar'), super_group = CONTENT_AND_UI ) +settings.register( + values.LongStringValue( + SIDEBAR_QUESTION, + 'QUESTION_PAGE_TOP_BANNER', + description = _('Top banner'), + default = '', + help_text = _( + 'When using this option, please ' + 'use the HTML validation service to make sure that ' + 'your input is valid and works well in all browsers.' + ) + ) +) + settings.register( values.LongStringValue( SIDEBAR_QUESTION, @@ -20,7 +34,7 @@ settings.register( default = '', help_text = _( 'Use this area to enter content at the TOP of the sidebar' - 'in HTML format. When using this option ' + 'in HTML format. When using this option ' '(as well as the sidebar footer), please ' 'use the HTML validation service to make sure that ' 'your input is valid and works well in all browsers.' diff --git a/askbot/doc/source/changelog.rst b/askbot/doc/source/changelog.rst index 7d1a2fc0..f1fe22e2 100644 --- a/askbot/doc/source/changelog.rst +++ b/askbot/doc/source/changelog.rst @@ -7,6 +7,7 @@ In development to help prevent user profile spam (Evgeny) * Added a function to create a custom user profile tab, the feature requires access to the server (Evgeny) +* Added optional top banner to the question page (Evgeny) 0.7.43 (May 14, 2012) --------------------- diff --git a/askbot/skins/default/templates/question.html b/askbot/skins/default/templates/question.html index f22796db..741b4bf4 100644 --- a/askbot/skins/default/templates/question.html +++ b/askbot/skins/default/templates/question.html @@ -156,6 +156,9 @@ {% endblock %} {% block content %} +
    + {{ settings.QUESTION_PAGE_TOP_BANNER }} +
    {% if is_cacheable %} {% cache long_time "thread-content-html" thread.id %} {% include "question/content.html" %} -- cgit v1.2.3-1-g7c22 From bd28283ee522ee5d1714960dab862011aea3d523 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Sun, 3 Jun 2012 12:33:27 -0400 Subject: fixed management commands that were still referring to the old model Question, which we replaced with Thread and Post --- askbot/management/commands/fix_answer_counts.py | 5 ++-- askbot/management/commands/fix_question_tags.py | 30 +++++++++++----------- .../management/commands/fix_revisionless_posts.py | 7 +++-- 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/askbot/management/commands/fix_answer_counts.py b/askbot/management/commands/fix_answer_counts.py index 959e37b6..9f22422e 100644 --- a/askbot/management/commands/fix_answer_counts.py +++ b/askbot/management/commands/fix_answer_counts.py @@ -23,6 +23,5 @@ class Command(NoArgsCommand): """function that handles the command job """ self.remove_save_signals() - questions = models.Question.objects.all() - for question in questions: - question.thread.update_answer_count() + for thread in models.Thread.objects.all(): + thread.update_answer_count() diff --git a/askbot/management/commands/fix_question_tags.py b/askbot/management/commands/fix_question_tags.py index 9858e397..d575e651 100644 --- a/askbot/management/commands/fix_question_tags.py +++ b/askbot/management/commands/fix_question_tags.py @@ -39,33 +39,33 @@ class Command(NoArgsCommand): transaction.commit() #go through questions and fix tag records on each - questions = models.Question.objects.all() + threads = models.Thread.objects.all() checked_count = 0 found_count = 0 - total_count = questions.count() + total_count = threads.count() print "Searching for questions with inconsistent tag records:", - for question in questions: - tags = question.thread.tags.all() - denorm_tag_set = set(question.get_tag_names()) - norm_tag_set = set(question.thread.tags.values_list('name', flat=True)) + for thread in threads: + tags = thread.tags.all() + denorm_tag_set = set(thread.get_tag_names()) + norm_tag_set = set(thread.tags.values_list('name', flat=True)) if norm_tag_set != denorm_tag_set: - if question.last_edited_by: - user = question.last_edited_by - timestamp = question.last_edited_at + if thread.last_edited_by: + user = thread.last_edited_by + timestamp = thread.last_edited_at else: - user = question.author - timestamp = question.added_at + user = thread.author + timestamp = thread.added_at - tagnames = forms.TagNamesField().clean(question.tagnames) + tagnames = forms.TagNamesField().clean(thread.tagnames) - question.thread.update_tags( + thread.update_tags( tagnames = tagnames, user = user, timestamp = timestamp ) - question.thread.tagnames = tagnames - question.thread.save() + thread.tagnames = tagnames + thread.save() found_count += 1 transaction.commit() diff --git a/askbot/management/commands/fix_revisionless_posts.py b/askbot/management/commands/fix_revisionless_posts.py index 92c03425..9535bef3 100644 --- a/askbot/management/commands/fix_revisionless_posts.py +++ b/askbot/management/commands/fix_revisionless_posts.py @@ -7,11 +7,11 @@ from django.db.models import signals, Count from askbot import models from askbot import const -def fix_revisionless_posts(post_class, post_name): +def fix_revisionless_posts(post_class): posts = post_class.objects.annotate( rev_count = Count('revisions') ).filter(rev_count = 0) - print 'have %d corrupted %ss' % (len(posts), post_name) + print 'have %d corrupted posts' % len(posts) for post in posts: post.add_revision( author = post.author, @@ -39,5 +39,4 @@ class Command(NoArgsCommand): """function that handles the command job """ self.remove_save_signals() - fix_revisionless_posts(models.Question, 'question') - fix_revisionless_posts(models.Answer, 'answer') + fix_revisionless_posts(models.Post) -- cgit v1.2.3-1-g7c22 From be9ce4b61578e7d99e4a458c495de6ce50574634 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Sun, 3 Jun 2012 14:11:45 -0400 Subject: fixed bug where creating a group could introduce a duplicate tag --- askbot/models/question.py | 2 ++ askbot/models/tag.py | 4 +++- askbot/tests/db_api_tests.py | 17 ++++++++++++++++- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/askbot/models/question.py b/askbot/models/question.py index dafe7ab0..affac74c 100644 --- a/askbot/models/question.py +++ b/askbot/models/question.py @@ -74,6 +74,7 @@ class ThreadManager(models.Manager): by_email = False, email_address = None ): + """creates new thread""" # TODO: Some of this code will go to Post.objects.create_new thread = super( @@ -794,6 +795,7 @@ class Thread(models.Model): return False def retag(self, retagged_by=None, retagged_at=None, tagnames=None, silent=False): + """changes thread tags""" if None in (retagged_by, retagged_at, tagnames): raise Exception('arguments retagged_at, retagged_by and tagnames are required') diff --git a/askbot/models/tag.py b/askbot/models/tag.py index 779c0b68..03c91162 100644 --- a/askbot/models/tag.py +++ b/askbot/models/tag.py @@ -105,7 +105,9 @@ class GroupTagManager(TagManager): #replace spaces with dashes group_name = clean_group_name(group_name) try: - tag = self.get(name = group_name) + #iexact is important!!! b/c we don't want case variants + #of tags + tag = self.get(name__iexact = group_name) except self.model.DoesNotExist: tag = self.model(name = group_name, created_by = user) tag.save() diff --git a/askbot/tests/db_api_tests.py b/askbot/tests/db_api_tests.py index 1f3d3b9b..3a0c9582 100644 --- a/askbot/tests/db_api_tests.py +++ b/askbot/tests/db_api_tests.py @@ -15,6 +15,9 @@ from askbot.conf import settings as askbot_settings import datetime class DBApiTests(AskbotTestCase): + """tests methods on User object, + that were added for askbot + """ def setUp(self): self.create_user() @@ -161,7 +164,8 @@ class DBApiTests(AskbotTestCase): matches = models.Post.objects.get_questions().get_by_text_query("database'") self.assertTrue(len(matches) == 1) -class UserLikeTests(AskbotTestCase): +class UserLikeTagTests(AskbotTestCase): + """tests for user liking and disliking tags""" def setUp(self): self.create_user() self.question = self.post_question(tags = 'one two three') @@ -387,3 +391,14 @@ class CommentTests(AskbotTestCase): self.other_user.upvote(comment, cancel = True) comment = models.Post.objects.get_comments().get(id = self.comment.id) self.assertEquals(comment.score, 0) + +class TagAndGroupTests(AskbotTestCase): + def setUp(self): + self.u1 = self.create_user('u1') + + def test_group_cannot_create_case_variant_tag(self): + self.post_question(user = self.u1, tags = 'one two three') + models.Tag.group_tags.get_or_create(user = self.u1, group_name = 'One') + tag_one = models.Tag.objects.filter(name__iexact = 'one') + self.assertEqual(tag_one.count(), 1) + self.assertEqual(tag_one[0].name, 'one') -- cgit v1.2.3-1-g7c22 From f2733573a87049e416bac727035c21013a9e80af Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Sun, 3 Jun 2012 17:43:31 -0400 Subject: hopefully fixed internal server error when google tries to load tag wiki text --- askbot/tests/page_load_tests.py | 19 ++++++++++++++++++- askbot/tests/utils.py | 12 ++++++++++++ askbot/views/commands.py | 3 +++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/askbot/tests/page_load_tests.py b/askbot/tests/page_load_tests.py index ebfba0c3..e3e699a7 100644 --- a/askbot/tests/page_load_tests.py +++ b/askbot/tests/page_load_tests.py @@ -67,7 +67,9 @@ class PageLoadTestCase(AskbotTestCase): def setUp(self): self.old_cache = cache.cache - cache.cache = DummyCache('', {}) # Disable caching (to not interfere with production cache, not sure if that's possible but let's not risk it) + #Disable caching (to not interfere with production cache, + #not sure if that's possible but let's not risk it) + cache.cache = DummyCache('', {}) def tearDown(self): cache.cache = self.old_cache # Restore caching @@ -610,3 +612,18 @@ class QuestionPageRedirectTests(AskbotTestCase): #point to a non-existing comment resp = self.client.get(url, data={'comment': 100301}) self.assertRedirects(resp, expected_url = self.q.get_absolute_url()) + +class CommandViewTests(AskbotTestCase): + def test_get_tag_wiki_text_succeeds(self): + tag1 = self.create_tag('tag1') + response = self.client.get( + reverse('load_tag_wiki_text'), + data = {'tag_id': tag1.id} + ) + self.assertEqual(response.status_code, 200) + + def test_get_tag_wiki_text_fails(self): + tag1 = self.create_tag('tag1') + response = self.client.get(reverse('load_tag_wiki_text')) + self.assertEqual(response.status_code, 400)#bad request + diff --git a/askbot/tests/utils.py b/askbot/tests/utils.py index fdeea371..d2cbaf73 100644 --- a/askbot/tests/utils.py +++ b/askbot/tests/utils.py @@ -170,6 +170,18 @@ class AskbotTestCase(TestCase): timestamp = timestamp ) + def create_tag(self, tag_name, user = None): + """creates a user, b/c it is necessary""" + if user is None: + try: + user = models.User.objects.get(username = 'tag_creator') + except models.User.DoesNotExist: + user = self.create_user('tag_creator') + + tag = models.Tag(created_by = user, name = tag_name) + tag.save() + return tag + def post_comment( self, user = None, diff --git a/askbot/views/commands.py b/askbot/views/commands.py index 6fd493cc..53c363ab 100644 --- a/askbot/views/commands.py +++ b/askbot/views/commands.py @@ -485,6 +485,9 @@ def get_tag_list(request): @decorators.get_only def load_tag_wiki_text(request): """returns text of the tag wiki in markdown format""" + if 'tag_id' not in request.GET: + return HttpResponse('', status = 400)#bad request + tag = get_object_or_404(models.Tag, id = request.GET['tag_id']) tag_wiki_text = getattr(tag.tag_wiki, 'text', '').strip() return HttpResponse(tag_wiki_text, mimetype = 'text/plain') -- cgit v1.2.3-1-g7c22 From bec0efc77383afaa7fac9d668a93467e9f22c065 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Tue, 5 Jun 2012 06:33:29 -0400 Subject: email meaningful feedback in re:ask by email when user does no have sufficient rep --- askbot/mail/__init__.py | 15 +++++++++++++++ askbot/models/__init__.py | 8 ++++---- .../email/insufficient_rep_to_post_by_email.html | 13 +++++++++++++ 3 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 askbot/skins/default/templates/email/insufficient_rep_to_post_by_email.html diff --git a/askbot/mail/__init__.py b/askbot/mail/__init__.py index a0086a0a..7e3abbf6 100644 --- a/askbot/mail/__init__.py +++ b/askbot/mail/__init__.py @@ -10,11 +10,13 @@ from django.core.exceptions import PermissionDenied from django.forms import ValidationError from django.utils.translation import ugettext_lazy as _ from django.utils.translation import string_concat +from django.template import Context from askbot import exceptions from askbot import const from askbot.conf import settings as askbot_settings from askbot.utils import url_utils from askbot.utils.file_utils import store_file +from askbot.skins.loaders import get_template #todo: maybe send_mail functions belong to models #or the future API def prefix_the_subject_line(subject): @@ -331,6 +333,19 @@ def process_emailed_question( if user.email_isvalid == False: raise PermissionDenied('Lacking email signature') + if user.can_post_by_email() == False: + #todo: factor this code out + template = get_template('email/insufficient_rep_to_post_by_email.html') + min_rep = askbot_settings.MIN_REP_TO_POST_BY_EMAIL + min_upvotes = min_rep / askbot_settings.REP_GAIN_FOR_RECEIVING_UPVOTE + data = { + 'username': user.username, + 'site_name': askbot_settings.APP_SHORT_NAME, + 'min_upvotes': min_upvotes + } + message = template.render(Context(data)) + raise PermissionDenied(message) + tagnames = form.cleaned_data['tagnames'] title = form.cleaned_data['title'] body_text = form.cleaned_data['body_text'] diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py index 3319b9ae..b686818f 100644 --- a/askbot/models/__init__.py +++ b/askbot/models/__init__.py @@ -297,7 +297,7 @@ def user_can_have_strong_url(self): followed by the search engine crawlers""" return (self.reputation >= askbot_settings.MIN_REP_TO_HAVE_STRONG_URL) -def user_can_reply_by_email(self): +def user_can_post_by_email(self): """True, if reply by email is enabled and user has sufficient reputatiton""" return askbot_settings.REPLY_BY_EMAIL and \ @@ -2519,7 +2519,7 @@ User.add_to_class('is_following_question', user_is_following_question) User.add_to_class('mark_tags', user_mark_tags) User.add_to_class('update_response_counts', user_update_response_counts) User.add_to_class('can_have_strong_url', user_can_have_strong_url) -User.add_to_class('can_reply_by_email', user_can_reply_by_email) +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('is_administrator', user_is_administrator) User.add_to_class('is_administrator_or_moderator', user_is_administrator_or_moderator) @@ -2687,7 +2687,7 @@ def format_instant_notification_email( 'post_link': '%s' % (post_url, _(post.post_type)) } - can_reply = to_user.can_reply_by_email() + can_reply = to_user.can_post_by_email() if can_reply: reply_separator = const.SIMPLE_REPLY_SEPARATOR_TEMPLATE % \ @@ -2746,7 +2746,7 @@ def get_reply_to_addresses(user, post): #these variables will contain return values primary_addr = django_settings.DEFAULT_FROM_EMAIL secondary_addr = None - if user.can_reply_by_email(): + if user.can_post_by_email(): if user.reputation >= askbot_settings.MIN_REP_TO_POST_BY_EMAIL: reply_args = { diff --git a/askbot/skins/default/templates/email/insufficient_rep_to_post_by_email.html b/askbot/skins/default/templates/email/insufficient_rep_to_post_by_email.html new file mode 100644 index 00000000..071cbbdf --- /dev/null +++ b/askbot/skins/default/templates/email/insufficient_rep_to_post_by_email.html @@ -0,0 +1,13 @@ +{% import "email/macros.html" as macros %} +{# parameters: + * min_upvotes + * username + * site_name - for the footer +#} +

    + {% trans %}{{ username }}, your question could not be posted by email just yet{% endtrans %} +

    +

    + {% trans %}To make posts by email, you need to receive about {{min_upvotes}} upvotes.{% endtrans %} +

    +{% include "email/footer.html" %} -- cgit v1.2.3-1-g7c22 From 59af35e4a754b50c801c8dc25aeadd06eaef35a4 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Tue, 5 Jun 2012 06:58:29 -0400 Subject: small corrections in the bounce email --- askbot/mail/__init__.py | 7 ++++--- .../default/templates/email/insufficient_rep_to_post_by_email.html | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/askbot/mail/__init__.py b/askbot/mail/__init__.py index 7e3abbf6..ddf909a7 100644 --- a/askbot/mail/__init__.py +++ b/askbot/mail/__init__.py @@ -208,7 +208,7 @@ def bounce_email(email, subject, reason = None, body_text = None): 'site': askbot_settings.APP_SHORT_NAME, 'url': url_utils.get_login_url() } - elif reason == 'permission_denied': + elif reason == 'permission_denied' and body_text: error_message = _( '

    Sorry, your question could not be posted ' 'due to insufficient privileges of your user account

    ' @@ -216,7 +216,7 @@ def bounce_email(email, subject, reason = None, body_text = None): else: raise ValueError('unknown reason to bounce an email: "%s"' % reason) - if body_text != None: + if body_text: error_message = string_concat(error_message, body_text) #print 'sending email' @@ -337,7 +337,8 @@ def process_emailed_question( #todo: factor this code out template = get_template('email/insufficient_rep_to_post_by_email.html') min_rep = askbot_settings.MIN_REP_TO_POST_BY_EMAIL - min_upvotes = min_rep / askbot_settings.REP_GAIN_FOR_RECEIVING_UPVOTE + min_upvotes = 1 + \ + (min_rep/askbot_settings.REP_GAIN_FOR_RECEIVING_UPVOTE) data = { 'username': user.username, 'site_name': askbot_settings.APP_SHORT_NAME, diff --git a/askbot/skins/default/templates/email/insufficient_rep_to_post_by_email.html b/askbot/skins/default/templates/email/insufficient_rep_to_post_by_email.html index 071cbbdf..0abd8e63 100644 --- a/askbot/skins/default/templates/email/insufficient_rep_to_post_by_email.html +++ b/askbot/skins/default/templates/email/insufficient_rep_to_post_by_email.html @@ -4,7 +4,7 @@ * username * site_name - for the footer #} -

    +

    {% trans %}{{ username }}, your question could not be posted by email just yet{% endtrans %}

    -- cgit v1.2.3-1-g7c22 From 481c042fe2811db09c591fe9536cb0f5c9249655 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Tue, 5 Jun 2012 07:31:38 -0400 Subject: a small fix to the email format --- askbot/mail/__init__.py | 5 ++++- .../email/insufficient_rep_to_post_by_email.html | 4 +++- askbot/utils/html.py | 15 ++++++++++++++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/askbot/mail/__init__.py b/askbot/mail/__init__.py index ddf909a7..4c5a4acb 100644 --- a/askbot/mail/__init__.py +++ b/askbot/mail/__init__.py @@ -15,6 +15,7 @@ from askbot import exceptions from askbot import const from askbot.conf import settings as askbot_settings from askbot.utils import url_utils +from askbot.utils import html as html_utils from askbot.utils.file_utils import store_file from askbot.skins.loaders import get_template #todo: maybe send_mail functions belong to models @@ -208,7 +209,7 @@ def bounce_email(email, subject, reason = None, body_text = None): 'site': askbot_settings.APP_SHORT_NAME, 'url': url_utils.get_login_url() } - elif reason == 'permission_denied' and body_text: + elif reason == 'permission_denied' and body_text is None: error_message = _( '

    Sorry, your question could not be posted ' 'due to insufficient privileges of your user account

    ' @@ -339,9 +340,11 @@ def process_emailed_question( min_rep = askbot_settings.MIN_REP_TO_POST_BY_EMAIL min_upvotes = 1 + \ (min_rep/askbot_settings.REP_GAIN_FOR_RECEIVING_UPVOTE) + site_link = html_utils.site_link('ask', askbot_settings.SITE_NAME) data = { 'username': user.username, 'site_name': askbot_settings.APP_SHORT_NAME, + 'site_link': site_link, 'min_upvotes': min_upvotes } message = template.render(Context(data)) diff --git a/askbot/skins/default/templates/email/insufficient_rep_to_post_by_email.html b/askbot/skins/default/templates/email/insufficient_rep_to_post_by_email.html index 0abd8e63..0b953c31 100644 --- a/askbot/skins/default/templates/email/insufficient_rep_to_post_by_email.html +++ b/askbot/skins/default/templates/email/insufficient_rep_to_post_by_email.html @@ -3,11 +3,13 @@ * min_upvotes * username * site_name - for the footer + * site_link - html for the link #}

    - {% trans %}{{ username }}, your question could not be posted by email just yet{% endtrans %} + {% trans %}{{ username }}, your question could not be posted by email just yet.{% endtrans %}

    {% trans %}To make posts by email, you need to receive about {{min_upvotes}} upvotes.{% endtrans %} + {% trans %}Please post your question at {{ site_link|safe }}{% endtrans %}

    {% include "email/footer.html" %} diff --git a/askbot/utils/html.py b/askbot/utils/html.py index f91fa980..1ce3ad35 100644 --- a/askbot/utils/html.py +++ b/askbot/utils/html.py @@ -1,7 +1,10 @@ """Utilities for working with HTML.""" import html5lib from html5lib import sanitizer, serializer, tokenizer, treebuilders, treewalkers -import re, htmlentitydefs +import re +import htmlentitydefs +from urlparse import urlparse +from django.core.urlresolvers import reverse class HTMLSanitizerMixin(sanitizer.HTMLSanitizerMixin): acceptable_elements = ('a', 'abbr', 'acronym', 'address', 'b', 'big', @@ -51,6 +54,16 @@ def sanitize_html(html): output_generator = s.serialize(stream) return u''.join(output_generator) +def site_link(url_name, title): + """returns html for the link to the given url + todo: may be improved to process url parameters, keyword + and other arguments + """ + from askbot.conf import settings + base_url = urlparse(settings.APP_URL) + url = base_url.scheme + '://' + base_url.netloc + reverse(url_name) + return '%s' % (url, title) + def unescape(text): """source: http://effbot.org/zone/re-sub.htm#unescape-html Removes HTML or XML character references and entities from a text string. -- cgit v1.2.3-1-g7c22 From 884147ddd821deb0a9c7490606c3c537753811d0 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Tue, 5 Jun 2012 07:56:12 -0400 Subject: used a nonexisting setting --- askbot/mail/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/askbot/mail/__init__.py b/askbot/mail/__init__.py index 4c5a4acb..01f206d0 100644 --- a/askbot/mail/__init__.py +++ b/askbot/mail/__init__.py @@ -340,7 +340,10 @@ def process_emailed_question( min_rep = askbot_settings.MIN_REP_TO_POST_BY_EMAIL min_upvotes = 1 + \ (min_rep/askbot_settings.REP_GAIN_FOR_RECEIVING_UPVOTE) - site_link = html_utils.site_link('ask', askbot_settings.SITE_NAME) + site_link = html_utils.site_link( + 'ask', + askbot_settings.APP_SHORT_NAME + ) data = { 'username': user.username, 'site_name': askbot_settings.APP_SHORT_NAME, -- cgit v1.2.3-1-g7c22 From 84260347020e33412b295140af51f7d17ac07343 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Tue, 5 Jun 2012 08:23:41 -0400 Subject: fixed email html template --- .../default/templates/email/insufficient_rep_to_post_by_email.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/askbot/skins/default/templates/email/insufficient_rep_to_post_by_email.html b/askbot/skins/default/templates/email/insufficient_rep_to_post_by_email.html index 0b953c31..d65ad937 100644 --- a/askbot/skins/default/templates/email/insufficient_rep_to_post_by_email.html +++ b/askbot/skins/default/templates/email/insufficient_rep_to_post_by_email.html @@ -10,6 +10,6 @@

    {% trans %}To make posts by email, you need to receive about {{min_upvotes}} upvotes.{% endtrans %} - {% trans %}Please post your question at {{ site_link|safe }}{% endtrans %} + {% trans link=site_link|safe %}Please post your question at {{link}}{% endtrans %}

    {% include "email/footer.html" %} -- cgit v1.2.3-1-g7c22 From 5bfb305f7bb695f8208a7c64c441ff4fc335caf2 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Tue, 5 Jun 2012 08:38:12 -0400 Subject: fixed format of bounce email --- askbot/mail/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/askbot/mail/__init__.py b/askbot/mail/__init__.py index 01f206d0..2f8d7c79 100644 --- a/askbot/mail/__init__.py +++ b/askbot/mail/__init__.py @@ -214,11 +214,11 @@ def bounce_email(email, subject, reason = None, body_text = None): '

    Sorry, your question could not be posted ' 'due to insufficient privileges of your user account

    ' ) + elif body_text: + error_message = body_text else: raise ValueError('unknown reason to bounce an email: "%s"' % reason) - if body_text: - error_message = string_concat(error_message, body_text) #print 'sending email' #print email -- cgit v1.2.3-1-g7c22 From 45ee8c6abd04540938713cc7d5ab2f4bb2aeac7e Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Tue, 5 Jun 2012 08:41:34 -0400 Subject: one more small formatting fix --- .../default/templates/email/insufficient_rep_to_post_by_email.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/askbot/skins/default/templates/email/insufficient_rep_to_post_by_email.html b/askbot/skins/default/templates/email/insufficient_rep_to_post_by_email.html index d65ad937..da4c93ca 100644 --- a/askbot/skins/default/templates/email/insufficient_rep_to_post_by_email.html +++ b/askbot/skins/default/templates/email/insufficient_rep_to_post_by_email.html @@ -9,7 +9,7 @@ {% trans %}{{ username }}, your question could not be posted by email just yet.{% endtrans %}

    - {% trans %}To make posts by email, you need to receive about {{min_upvotes}} upvotes.{% endtrans %} - {% trans link=site_link|safe %}Please post your question at {{link}}{% endtrans %} + {% trans %}To make posts by email, you need to receive about {{min_upvotes}} upvotes.{% endtrans %}
    + {% trans link=site_link|safe %}At this time, please post your question at {{link}}{% endtrans %}

    {% include "email/footer.html" %} -- cgit v1.2.3-1-g7c22 From b15a2e3643384b48f5f9594fc745012292ee3593 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Tue, 5 Jun 2012 09:48:30 -0400 Subject: created askbot.mail.messages module and made "no signature" email respondable --- askbot/mail/__init__.py | 46 ++++++++++----------- askbot/mail/messages.py | 48 ++++++++++++++++++++++ .../default/templates/email/ask_for_signature.html | 11 +++++ 3 files changed, 82 insertions(+), 23 deletions(-) create mode 100644 askbot/mail/messages.py create mode 100644 askbot/skins/default/templates/email/ask_for_signature.html diff --git a/askbot/mail/__init__.py b/askbot/mail/__init__.py index 2f8d7c79..299acf22 100644 --- a/askbot/mail/__init__.py +++ b/askbot/mail/__init__.py @@ -14,10 +14,9 @@ from django.template import Context from askbot import exceptions from askbot import const from askbot.conf import settings as askbot_settings +from askbot import models from askbot.utils import url_utils -from askbot.utils import html as html_utils from askbot.utils.file_utils import store_file -from askbot.skins.loaders import get_template #todo: maybe send_mail functions belong to models #or the future API def prefix_the_subject_line(subject): @@ -167,7 +166,9 @@ TAGS_INSTRUCTION_FOOTNOTE = _( the tags, use a semicolon or a comma, for example, [One tag; Other tag]

    """ ) -def bounce_email(email, subject, reason = None, body_text = None): +def bounce_email( + email, subject, reason = None, body_text = None, reply_to = None +): """sends a bounce email at address ``email``, with the subject line ``subject``, accepts several reasons for the bounce: * ``'problem_posting'``, ``unknown_user`` and ``permission_denied`` @@ -224,10 +225,15 @@ def bounce_email(email, subject, reason = None, body_text = None): #print email #print subject #print error_message + headers = {} + if reply_to: + headers['Reply-To'] = reply_to + send_mail( recipient_list = (email,), subject_line = 'Re: ' + subject, - body_text = error_message + body_text = error_message, + headers = headers ) def extract_reply(text): @@ -316,7 +322,9 @@ def process_emailed_question( #a bunch of imports here, to avoid potential circular import issues from askbot.forms import AskByEmailForm from askbot.models import User + from askbot.mail import messages + reply_to = None try: #todo: delete uploaded files when posting by email fails!!! data = { @@ -332,26 +340,17 @@ def process_emailed_question( ) if user.email_isvalid == False: - raise PermissionDenied('Lacking email signature') + reply_to = models.ReplyAddress.objects.create_new( + user = user, + reply_action = 'validate_email' + ).as_email_address() + raise PermissionDenied( + messages.ask_for_signature(user, footer_code = reply_to), + reply_to = reply_to.as_email_address() + ) if user.can_post_by_email() == False: - #todo: factor this code out - template = get_template('email/insufficient_rep_to_post_by_email.html') - min_rep = askbot_settings.MIN_REP_TO_POST_BY_EMAIL - min_upvotes = 1 + \ - (min_rep/askbot_settings.REP_GAIN_FOR_RECEIVING_UPVOTE) - site_link = html_utils.site_link( - 'ask', - askbot_settings.APP_SHORT_NAME - ) - data = { - 'username': user.username, - 'site_name': askbot_settings.APP_SHORT_NAME, - 'site_link': site_link, - 'min_upvotes': min_upvotes - } - message = template.render(Context(data)) - raise PermissionDenied(message) + raise PermissionDenied(messages.insufficient_reputation(user)) tagnames = form.cleaned_data['tagnames'] title = form.cleaned_data['title'] @@ -385,7 +384,8 @@ def process_emailed_question( email_address, subject, reason = 'permission_denied', - body_text = unicode(error) + body_text = unicode(error), + reply_to = reply_to ) except ValidationError: if from_address: diff --git a/askbot/mail/messages.py b/askbot/mail/messages.py new file mode 100644 index 00000000..652a8b11 --- /dev/null +++ b/askbot/mail/messages.py @@ -0,0 +1,48 @@ +"""functions in this module return body text +of email messages for various occasions +""" +import functools +from django.template import Context +from askbot.conf import settings as askbot_settings +from askbot.skins.loaders import get_template +from askbot.utils import html as html_utils + +def message(func, template = None): + """a decorator that creates a function + which returns formatted message using the + template and data""" + @functools.wraps(func) + def wrapped(data): + template = get_template(template) + return template.render(Context(data)) + +@message('email/ask_for_signature.html') +def ask_for_signature(user, footer_code = None): + """tells that we don't have user's signature + and because of that he/she cannot make posts + the message will ask to make a simple response + """ + return { + 'username': user.username, + 'site_name': askbot_settings.APP_SHORT_NAME, + 'footer_code': footer_code + } + +@message('email/insufficient_rep_to_post_by_email.html') +def insufficient_reputation(user): + """tells user that he does not have + enough rep and suggests to ask on the web + """ + min_rep = askbot_settings.MIN_REP_TO_POST_BY_EMAIL + min_upvotes = 1 + \ + (min_rep/askbot_settings.REP_GAIN_FOR_RECEIVING_UPVOTE) + site_link = html_utils.site_link( + 'ask', + askbot_settings.APP_SHORT_NAME + ) + return { + 'username': user.username, + 'site_name': askbot_settings.APP_SHORT_NAME, + 'site_link': site_link, + 'min_upvotes': min_upvotes + } diff --git a/askbot/skins/default/templates/email/ask_for_signature.html b/askbot/skins/default/templates/email/ask_for_signature.html new file mode 100644 index 00000000..4fdec833 --- /dev/null +++ b/askbot/skins/default/templates/email/ask_for_signature.html @@ -0,0 +1,11 @@ +{% import "email/macros.html" as macros %} +

    + {% trans %}{{ username }}, please reply to this message.{% endtrans %} +

    +

    + {% trans %}Your post could not be published, because we could not detect signature in your email.{% endtrans %}
    + {% trans %}Please make a simple response, without editing this message.{% endtrans %}
    + {% trans %}We will attempt to detect the signature in your response.{% endtrans %} +

    +{% include "email/footer.html" %} +

    {{ footer_code }}

    -- cgit v1.2.3-1-g7c22 From a23d35e7a111215438c6bbb6a022154f028c68bd Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Tue, 5 Jun 2012 10:06:56 -0400 Subject: fixed an import error --- askbot/mail/__init__.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/askbot/mail/__init__.py b/askbot/mail/__init__.py index 299acf22..81829049 100644 --- a/askbot/mail/__init__.py +++ b/askbot/mail/__init__.py @@ -14,7 +14,6 @@ from django.template import Context from askbot import exceptions from askbot import const from askbot.conf import settings as askbot_settings -from askbot import models from askbot.utils import url_utils from askbot.utils.file_utils import store_file #todo: maybe send_mail functions belong to models @@ -321,7 +320,7 @@ def process_emailed_question( """posts question received by email or bounces the message""" #a bunch of imports here, to avoid potential circular import issues from askbot.forms import AskByEmailForm - from askbot.models import User + from askbot.models import ReplyAddress, User from askbot.mail import messages reply_to = None @@ -339,8 +338,11 @@ def process_emailed_question( email__iexact = email_address ) + if user.can_post_by_email() == False: + raise PermissionDenied(messages.insufficient_reputation(user)) + if user.email_isvalid == False: - reply_to = models.ReplyAddress.objects.create_new( + reply_to = ReplyAddress.objects.create_new( user = user, reply_action = 'validate_email' ).as_email_address() @@ -349,9 +351,6 @@ def process_emailed_question( reply_to = reply_to.as_email_address() ) - if user.can_post_by_email() == False: - raise PermissionDenied(messages.insufficient_reputation(user)) - tagnames = form.cleaned_data['tagnames'] title = form.cleaned_data['title'] body_text = form.cleaned_data['body_text'] -- cgit v1.2.3-1-g7c22 From c065ba4de20c423eef9a834d416886f7466c16fc Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Tue, 5 Jun 2012 11:29:52 -0400 Subject: made the message() decorator for askbot.mail.messages work --- askbot/mail/messages.py | 18 +++++++++++------- .../default/templates/email/ask_for_signature.html | 4 ++-- askbot/skins/default/templates/email/macros.html | 4 ++++ askbot/tests/email_alert_tests.py | 8 ++++++++ 4 files changed, 25 insertions(+), 9 deletions(-) diff --git a/askbot/mail/messages.py b/askbot/mail/messages.py index 652a8b11..24091971 100644 --- a/askbot/mail/messages.py +++ b/askbot/mail/messages.py @@ -7,16 +7,20 @@ from askbot.conf import settings as askbot_settings from askbot.skins.loaders import get_template from askbot.utils import html as html_utils -def message(func, template = None): +def message(template = None): """a decorator that creates a function which returns formatted message using the template and data""" - @functools.wraps(func) - def wrapped(data): - template = get_template(template) - return template.render(Context(data)) + def decorate(func): + @functools.wraps(func) + def wrapped(*args, **kwargs): + template_object = get_template(template) + data = func(*args, **kwargs) + return template_object.render(Context(data)) + return wrapped + return decorate -@message('email/ask_for_signature.html') +@message(template = 'email/ask_for_signature.html') def ask_for_signature(user, footer_code = None): """tells that we don't have user's signature and because of that he/she cannot make posts @@ -28,7 +32,7 @@ def ask_for_signature(user, footer_code = None): 'footer_code': footer_code } -@message('email/insufficient_rep_to_post_by_email.html') +@message(template = 'email/insufficient_rep_to_post_by_email.html') def insufficient_reputation(user): """tells user that he does not have enough rep and suggests to ask on the web diff --git a/askbot/skins/default/templates/email/ask_for_signature.html b/askbot/skins/default/templates/email/ask_for_signature.html index 4fdec833..7aac4e5e 100644 --- a/askbot/skins/default/templates/email/ask_for_signature.html +++ b/askbot/skins/default/templates/email/ask_for_signature.html @@ -1,5 +1,5 @@ {% import "email/macros.html" as macros %} -

    +

    {% trans %}{{ username }}, please reply to this message.{% endtrans %}

    @@ -8,4 +8,4 @@ {% trans %}We will attempt to detect the signature in your response.{% endtrans %}

    {% include "email/footer.html" %} -

    {{ footer_code }}

    +

    {{ footer_code }}

    diff --git a/askbot/skins/default/templates/email/macros.html b/askbot/skins/default/templates/email/macros.html index 577ed753..1acbf515 100644 --- a/askbot/skins/default/templates/email/macros.html +++ b/askbot/skins/default/templates/email/macros.html @@ -90,3 +90,7 @@ {% macro heading_style() %} font-size:14px;font-weight:bold;margin-bottom:0px; {% endmacro %} + +{% macro fine_print_style() %} +font-size:8px;color:#aaa;margin-bottom:0px; +{% endmacro %} diff --git a/askbot/tests/email_alert_tests.py b/askbot/tests/email_alert_tests.py index f07a6613..9ec1a412 100644 --- a/askbot/tests/email_alert_tests.py +++ b/askbot/tests/email_alert_tests.py @@ -1005,3 +1005,11 @@ class PostApprovalTests(utils.AskbotTestCase): #moderation notification self.assertEquals(outbox[0].recipients(), [u1.email,]) self.assertEquals(outbox[1].recipients(), [u1.email,])#approval + + +class MailMessagesTests(utils.AskbotTestCase): + def test_ask_for_signature(self): + from askbot.mail import messages + user = self.create_user('user') + message = messages.ask_for_signature(user, footer_code = 'nothing') + self.assertTrue(user.username in message) -- cgit v1.2.3-1-g7c22 From 58f77f62281037c27bf55533527f6263d0136abc Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Tue, 5 Jun 2012 12:16:06 -0400 Subject: fixed a bug in askbot.mail module --- askbot/mail/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/askbot/mail/__init__.py b/askbot/mail/__init__.py index 81829049..f7619964 100644 --- a/askbot/mail/__init__.py +++ b/askbot/mail/__init__.py @@ -348,7 +348,7 @@ def process_emailed_question( ).as_email_address() raise PermissionDenied( messages.ask_for_signature(user, footer_code = reply_to), - reply_to = reply_to.as_email_address() + reply_to = reply_to ) tagnames = form.cleaned_data['tagnames'] -- cgit v1.2.3-1-g7c22 From 5683acc0c0bfe2e22a7c31d3316d1b09fadd43d0 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Tue, 5 Jun 2012 12:29:33 -0400 Subject: finally signature validation upon asking works --- askbot/mail/__init__.py | 6 ++---- askbot/skins/default/templates/email/ask_for_signature.html | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/askbot/mail/__init__.py b/askbot/mail/__init__.py index f7619964..7ff8a2d3 100644 --- a/askbot/mail/__init__.py +++ b/askbot/mail/__init__.py @@ -346,10 +346,8 @@ def process_emailed_question( user = user, reply_action = 'validate_email' ).as_email_address() - raise PermissionDenied( - messages.ask_for_signature(user, footer_code = reply_to), - reply_to = reply_to - ) + message = messages.ask_for_signature(user, footer_code = reply_to) + raise PermissionDenied(message) tagnames = form.cleaned_data['tagnames'] title = form.cleaned_data['title'] diff --git a/askbot/skins/default/templates/email/ask_for_signature.html b/askbot/skins/default/templates/email/ask_for_signature.html index 7aac4e5e..e4449433 100644 --- a/askbot/skins/default/templates/email/ask_for_signature.html +++ b/askbot/skins/default/templates/email/ask_for_signature.html @@ -5,7 +5,7 @@

    {% trans %}Your post could not be published, because we could not detect signature in your email.{% endtrans %}
    {% trans %}Please make a simple response, without editing this message.{% endtrans %}
    - {% trans %}We will attempt to detect the signature in your response.{% endtrans %} + {% trans %}We will then attempt to detect the signature in your response and you should be able to post.{% endtrans %}

    {% include "email/footer.html" %}

    {{ footer_code }}

    -- cgit v1.2.3-1-g7c22 From cb58a484bd9dcc0f516d724d4ac7f39ddd1a3b2b Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Tue, 5 Jun 2012 13:06:58 -0400 Subject: added feedback email on email validation flow with reply- email address prefix --- askbot/mail/lamson_handlers.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/askbot/mail/lamson_handlers.py b/askbot/mail/lamson_handlers.py index 467493c7..f7053414 100644 --- a/askbot/mail/lamson_handlers.py +++ b/askbot/mail/lamson_handlers.py @@ -261,6 +261,7 @@ def PROCESS( parts = None, reply_address_object = None, subject_line = None, + from_address = None, **kwargs ): """handler to process the emailed message @@ -300,3 +301,17 @@ def PROCESS( robj.edit_post(body_text, edit_response = True) else: robj.create_reply(body_text) + elif robj.reply_action == 'validate_email': + #todo: this is copy-paste - factor it out to askbot.mail.messages + data = { + 'site_name': askbot_settings.APP_SHORT_NAME, + 'site_url': askbot_settings.APP_URL, + 'ask_address': 'ask@' + askbot_settings.REPLY_BY_EMAIL_HOSTNAME + } + template = get_template('email/re_welcome_lamson_on.html') + + mail.send_mail( + subject_line = _('Re: %s') % subject_line, + body_text = template.render(Context(data)), + recipient_list = [from_address,] + ) -- cgit v1.2.3-1-g7c22 From c2ea1fad96b8ea18677a2b802e102b36cf5a1430 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Tue, 5 Jun 2012 14:40:46 -0400 Subject: added send_respondable_welcome_email management command --- askbot/doc/source/management-commands.rst | 8 ++++++++ askbot/management/commands/send_respondable_welcome_email.py | 12 ++++++++++++ askbot/skins/default/templates/email/welcome_lamson_on.html | 5 +++-- 3 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 askbot/management/commands/send_respondable_welcome_email.py diff --git a/askbot/doc/source/management-commands.rst b/askbot/doc/source/management-commands.rst index f6c5beec..4efbef6e 100644 --- a/askbot/doc/source/management-commands.rst +++ b/askbot/doc/source/management-commands.rst @@ -104,6 +104,14 @@ Any configurable options, related to these commands are accessible via "Email" s +-------------------------------------+-------------------------------------------------------------+ | command | purpose | +=====================================+=============================================================+ +| `send_respondable_welcome_email` | Will send a respondable welcome email to **all** registered | +| | users whose email address was not validated. | +| | This feature requires "reply by email" enabled and "lamson" | +| | email processor installed on the system. | +| | The email will be respondable. When the user responds, | +| | askbot will validate the email and capture the signature in | +| | the end of the message. | ++-------------------------------------+-------------------------------------------------------------+ | `send_email_alerts` | Dispatches email alerts to the users according to | | | their subscription settings. This command does not | | | send instant" alerts because those are sent automatically | diff --git a/askbot/management/commands/send_respondable_welcome_email.py b/askbot/management/commands/send_respondable_welcome_email.py new file mode 100644 index 00000000..c386b531 --- /dev/null +++ b/askbot/management/commands/send_respondable_welcome_email.py @@ -0,0 +1,12 @@ +"""Management command that sends a respondable +welcome email to all users. +User's responses will be used to validate +the email addresses and extract the email signatures. +""" +from django.core.management.base import NoArgsCommand +from askbot.models import User, send_welcome_email + +class Command(NoArgsCommand): + def handle_noargs(self): + for user in User.objects.filter(email_isvalid = False): + send_welcome_email(user) diff --git a/askbot/skins/default/templates/email/welcome_lamson_on.html b/askbot/skins/default/templates/email/welcome_lamson_on.html index bcca4234..f49c5cb7 100644 --- a/askbot/skins/default/templates/email/welcome_lamson_on.html +++ b/askbot/skins/default/templates/email/welcome_lamson_on.html @@ -1,7 +1,8 @@ +{% import "email/macros.html" as macros" %} {# site_name - is short name of the site, email_code - address portion of the reply email used for this message, we scan to the last appearance of the email code to detect the response signature that will appear under #} -

    +

    {% trans %}Welcome to {{ site_name }}!{% endtrans %}

    @@ -11,4 +12,4 @@ of the email code to detect the response signature that will appear under #} {% trans %}Until we receive the response from you, you will not be able ask or answer questions on {{ site_name }} by email.{% endtrans %}

    {% include "email/footer.html" %} -

    {{ email_code }}

    {# important #} +

    {{ email_code }}

    {# important #} -- cgit v1.2.3-1-g7c22 From 6bfe9a140475ecd6b2dea5364a347206f8e2370e Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Tue, 5 Jun 2012 14:46:58 -0400 Subject: fixed bug in the template --- askbot/skins/default/templates/email/welcome_lamson_on.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/askbot/skins/default/templates/email/welcome_lamson_on.html b/askbot/skins/default/templates/email/welcome_lamson_on.html index f49c5cb7..0efa7096 100644 --- a/askbot/skins/default/templates/email/welcome_lamson_on.html +++ b/askbot/skins/default/templates/email/welcome_lamson_on.html @@ -1,4 +1,4 @@ -{% import "email/macros.html" as macros" %} +{% import "email/macros.html" as macros %} {# site_name - is short name of the site, email_code - address portion of the reply email used for this message, we scan to the last appearance of the email code to detect the response signature that will appear under #} -- cgit v1.2.3-1-g7c22 From 45335bccd66515997fc161d5f62550d260965a6b Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Wed, 6 Jun 2012 17:17:30 -0400 Subject: fixed bug where long tags were truncated in the retagger, as reported by user todofixit --- askbot/skins/common/media/js/post.js | 5 +++-- askbot/skins/common/media/js/utils.js | 4 +++- askbot/skins/default/templates/macros.html | 1 + 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/askbot/skins/common/media/js/post.js b/askbot/skins/common/media/js/post.js index 307d3be5..4f023d79 100644 --- a/askbot/skins/common/media/js/post.js +++ b/askbot/skins/common/media/js/post.js @@ -1062,10 +1062,11 @@ var questionRetagger = function(){ var tags_str = ''; links.each(function(index, element){ if (index === 0){ - tags_str = $(element).html(); + //this is pretty bad - we should use Tag.getName() + tags_str = $(element).attr('data-tag-name'); } else { - tags_str += ' ' + $(element).html(); + tags_str += ' ' + $(element).attr('data-tag-name'); } }); return tags_str; diff --git a/askbot/skins/common/media/js/utils.js b/askbot/skins/common/media/js/utils.js index 297e3f9a..69e0cb3a 100644 --- a/askbot/skins/common/media/js/utils.js +++ b/askbot/skins/common/media/js/utils.js @@ -989,7 +989,9 @@ Tag.prototype.decorate = function(element){ this._delete_icon.decorate(del); } this._inner_element = this._element.find('.tag'); - this._name = this.decodeTagName($.trim(this._inner_element.html())); + this._name = this.decodeTagName( + $.trim(this._inner_element.attr('data-tag-name')) + ); if (this._title !== null){ this._inner_element.attr('title', this._title); } diff --git a/askbot/skins/default/templates/macros.html b/askbot/skins/default/templates/macros.html index c8dc245f..831c148f 100644 --- a/askbot/skins/default/templates/macros.html +++ b/askbot/skins/default/templates/macros.html @@ -263,6 +263,7 @@ poor design of the data or methods on data objects #} title="{% trans %}see questions tagged '{{ tag }}'{% endtrans %}" {% endif %} rel="tag" + data-tag-name="{{ tag|replace('*', '✽')|escape }}" >{{ tag|replace('*', '✽')|truncate(20,True)}} {% if deletable %}
    Date: Wed, 6 Jun 2012 17:31:58 -0400 Subject: fixed styling of the retagger --- askbot/skins/default/media/style/style.less | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/askbot/skins/default/media/style/style.less b/askbot/skins/default/media/style/style.less index 4d758cea..af5bde12 100644 --- a/askbot/skins/default/media/style/style.less +++ b/askbot/skins/default/media/style/style.less @@ -3162,6 +3162,11 @@ ul.post-tags li { ul.post-retag { margin-bottom:0px; margin-left:5px; + input { + width: 400px; + height: 1.5em; + margin: 3px 0 0 -3px; + } } #question-controls .tags { -- cgit v1.2.3-1-g7c22 From 276eac48cc6ebeb4786eed34d13faa995321982d Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Wed, 6 Jun 2012 18:00:59 -0400 Subject: omitted tag truncation from all pages but the tags and user profile --- askbot/skins/default/templates/macros.html | 15 +++++++++++---- askbot/skins/default/templates/tags.html | 1 + .../skins/default/templates/user_profile/user_stats.html | 1 + 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/askbot/skins/default/templates/macros.html b/askbot/skins/default/templates/macros.html index 831c148f..08893ad0 100644 --- a/askbot/skins/default/templates/macros.html +++ b/askbot/skins/default/templates/macros.html @@ -200,7 +200,8 @@ poor design of the data or methods on data objects #} search_state = None, css_class = None, tag_css_class = None, - tag_html_tag = 'li' + tag_html_tag = 'li', + truncate_long_tags = False ) -%}