diff options
author | Evgeny Fadeev <evgeny.fadeev@gmail.com> | 2012-06-07 01:53:53 -0400 |
---|---|---|
committer | Evgeny Fadeev <evgeny.fadeev@gmail.com> | 2012-06-07 01:53:53 -0400 |
commit | 9f679e83639c642d903eeacde695baef1be44ec1 (patch) | |
tree | dd6c5772f77ac43c5044989959c6df678d16408b | |
parent | 428c91d505a4a09192f1bbc70f7f1c0c1429cc4b (diff) | |
parent | 84e1fc6f99879ede88f2d51df2b2730b5df6ff41 (diff) | |
download | askbot-9f679e83639c642d903eeacde695baef1be44ec1.tar.gz askbot-9f679e83639c642d903eeacde695baef1be44ec1.tar.bz2 askbot-9f679e83639c642d903eeacde695baef1be44ec1.zip |
made moderation dialog work again after the merge
61 files changed, 1246 insertions, 395 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, @@ -291,6 +289,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, 'REPLY_BY_EMAIL_HOSTNAME', @@ -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/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,21 +6,35 @@ 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, 'SIDEBAR_QUESTION_HEADER', description = _('Custom sidebar header'), 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/const/__init__.py b/askbot/const/__init__.py index f7255455..2bed1c31 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')), @@ -54,12 +55,51 @@ 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, ' - 'please use <a href="mailto://%(addr)s">this link</a>' + 'please use <a href="mailto:%(addr)s?subject=%(subject)s">this link</a>' ) -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/doc/source/changelog.rst b/askbot/doc/source/changelog.rst index 06808a9c..f975b83c 100644 --- a/askbot/doc/source/changelog.rst +++ b/askbot/doc/source/changelog.rst @@ -5,6 +5,11 @@ Development version ------------------- * Welcome email for the case when replying by email is enabled (Evgeny) * Detection of email signature based on the response to the welome email (Evgeny) +* Hide "website" and "about" section of the blocked user profiles + 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/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/doc/source/optional-modules.rst b/askbot/doc/source/optional-modules.rst index 0c013121..25bb5cc8 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 ===================== @@ -183,7 +226,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/forms.py b/askbot/forms.py index 81354cad..23da169a 100644 --- a/askbot/forms.py +++ b/askbot/forms.py @@ -9,7 +9,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/utils/mail.py b/askbot/mail/__init__.py index 4f653e6b..7ff8a2d3 100644 --- a/askbot/utils/mail.py +++ b/askbot/mail/__init__.py @@ -10,6 +10,7 @@ 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 @@ -164,7 +165,9 @@ TAGS_INSTRUCTION_FOOTNOTE = _( the tags, use a semicolon or a comma, for example, [One tag; Other tag]</p>""" ) -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`` @@ -206,25 +209,30 @@ 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 is None: error_message = _( '<p>Sorry, your question could not be posted ' 'due to insufficient privileges of your user account</p>' ) + elif body_text: + error_message = body_text 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 + 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): @@ -250,7 +258,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 and (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. @@ -266,7 +296,7 @@ def process_parts(parts): 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) @@ -274,25 +304,32 @@ 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): +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 + from askbot.models import ReplyAddress, User + from askbot.mail import messages + reply_to = None try: #todo: delete uploaded files when posting by email fails!!! - body, stored_files = process_parts(parts) data = { 'sender': from_address, 'subject': subject, - 'body_text': body + 'body_text': body_text } form = AskByEmailForm(data) if form.is_valid(): @@ -301,8 +338,16 @@ def process_emailed_question(from_address, subject, parts, tags = None): email__iexact = email_address ) + if user.can_post_by_email() == False: + raise PermissionDenied(messages.insufficient_reputation(user)) + if user.email_isvalid == False: - raise PermissionDenied('Lacking email signature') + reply_to = ReplyAddress.objects.create_new( + user = user, + reply_action = 'validate_email' + ).as_email_address() + message = messages.ask_for_signature(user, footer_code = reply_to) + raise PermissionDenied(message) tagnames = form.cleaned_data['tagnames'] title = form.cleaned_data['title'] @@ -336,7 +381,8 @@ def process_emailed_question(from_address, subject, parts, tags = None): 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/lamson_handlers.py b/askbot/mail/lamson_handlers.py index d7dc7e13..f7053414 100644 --- a/askbot/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 @@ -187,9 +187,13 @@ 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(from_address, subject, parts) + mail.process_emailed_question( + from_address, subject, body_text, stored_files + ) else: if askbot_settings.GROUP_EMAIL_ADDRESSES_ENABLED == False: return @@ -199,7 +203,8 @@ def ASK(message, host = None, addr = None): name__iexact = addr ) mail.process_emailed_question( - from_address, subject, parts, tags = [group_tag.name, ] + from_address, subject, body_text, stored_files, + tags = [group_tag.name, ] ) except Tag.DoesNotExist: #do nothing because this handler will match all emails @@ -220,28 +225,12 @@ def VALIDATE_EMAIL( the email signature todo: go a step further and """ - content, stored_files = mail.process_parts(parts) reply_code = reply_address_object.address - 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 + try: + content, stored_files, signature = mail.process_parts(parts, reply_code) user = reply_address_object.user - user.email_signature = signature + if signature and signature != user.email_signature: + user.email_signature = signature user.email_isvalid = True user.save() @@ -257,8 +246,7 @@ def VALIDATE_EMAIL( body_text = template.render(Context(data)), recipient_list = [from_address,] ) - - else: + except ValueError: raise ValueError( _( 'Please reply to the welcome email ' @@ -272,12 +260,58 @@ def VALIDATE_EMAIL( def PROCESS( parts = None, reply_address_object = None, + subject_line = None, + from_address = 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""" - if reply_address_object.was_used: - reply_address_object.edit_post(parts) - else: - reply_address_object.create_reply(parts) + #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) + + #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: + 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 + + #3) validate email address and save user + user.email_isvalid = True + user.save()#todo: actually, saving is not necessary, if nothing changed + + #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 == '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, 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,] + ) diff --git a/askbot/mail/messages.py b/askbot/mail/messages.py new file mode 100644 index 00000000..24091971 --- /dev/null +++ b/askbot/mail/messages.py @@ -0,0 +1,52 @@ +"""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(template = None): + """a decorator that creates a function + which returns formatted message using the + template and 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(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 + 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(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 + """ + 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/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) 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_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/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 0855abcd..b686818f 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""" @@ -207,9 +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.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): @@ -294,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 \ @@ -1249,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() @@ -1500,18 +1508,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) @@ -1870,6 +1878,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() @@ -2283,12 +2299,18 @@ def user_approve_post_revision(user, post_revision, timestamp = None): post = post_revision.post post.approved = True post.save() + if post_revision.post.post_type == 'question': thread = post.thread thread.approved = True thread.save() post.thread.invalidate_cached_data() + #send the signal of published 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): if cancel_all: @@ -2497,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) @@ -2569,12 +2591,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_address = None, + alt_reply_address = None, update_type = None, template = None, ): @@ -2621,16 +2644,16 @@ 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 = '<b><u style="background-color:#cfc">', - ins_end = '</u></b>', - del_start = '<del style="color:#600;background-color:#fcc">', - del_end = '</del>' - ) + revisions[1].html, + revisions[0].html, + ins_start = '<b><u style="background-color:#cfc">', + ins_end = '</u></b>', + del_start = '<del style="color:#600;background-color:#fcc">', + del_end = '</del>' + ) #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() @@ -2664,16 +2687,21 @@ def format_instant_notification_email( 'post_link': '<a href="%s">%s</a>' % (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.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 += '<br>' + const.REPLY_WITH_COMMENT_TEMPLATE % data + 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, + 'subject': urllib.quote( + ('Re: ' + post.thread.title).encode('utf-8') + ) + } + reply_separator += '<p>' + \ + const.REPLY_WITH_COMMENT_TEMPLATE % data + reply_separator += '</p>' else: reply_separator = user_action @@ -2691,7 +2719,56 @@ def format_instant_notification_email( 'reply_separator': reply_separator } subject_line = _('"%(title)s"') % {'title': origin_post.thread.title} - return subject_line, template.render(Context(update_data)) + + content = template.render(Context(update_data)) + if can_reply: + content += '<p style="font-size:8px;color:#aaa">' + \ + reply_address + '</p>' + + 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 user.can_post_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_answer' + + 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( @@ -2715,52 +2792,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_with_comment_address = 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: - - 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) - elif post.post_type == 'question': - reply_with_comment_address = ReplyAddress.objects.create_new(**reply_args) - #default action is to post answer - reply_args['reply_action'] = 'post_answer' - reply_addr = ReplyAddress.objects.create_new(**reply_args) - - 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_with_comment_address, + 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, @@ -2770,6 +2826,18 @@ def send_instant_notifications_about_activity_in_post( headers = headers ) +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 + """ + #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) + + #todo: move to utils def calculate_gravatar_hash(instance, **kwargs): """Calculates a User's gravatar hash from their email address.""" @@ -2815,14 +2883,6 @@ def record_post_update_activity( created = created, diff = diff, ) - #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): @@ -3167,22 +3227,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) @@ -3207,6 +3255,10 @@ 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) #set up a possibility for the users to follow others diff --git a/askbot/models/post.py b/askbot/models/post.py index b6ee6d2a..673b4c6f 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 <Activity> 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: @@ -442,51 +442,51 @@ 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(self, author = None, **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() + 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, diff = diff, - sender = post.__class__ + sender = self.__class__ ) try: @@ -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' @@ -611,40 +604,26 @@ 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 = '<div>' - for tag_name in self.get_tag_names(): - output += '<span style="%s">%s</span>' % (tag_style, tag_name) - output += '</div>' - return output - - 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? """ - from askbot.templatetags.extra_filters_jinja import absolutize_urls_func - output = '' - if self.post_type == 'question': - output += '<b>%s</b><br/>' % 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 = '<div style="%s">%s</div>' % (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), + 'is_leaf_post': is_leaf_post, + 'format': format + } + return template.render(Context(data)) def format_for_email_as_parent_thread_summary(self): """format for email as summary of parent posts @@ -657,47 +636,32 @@ class Post(models.Model): if parent_post is None: break quote_level += 1 + """ + output += '<p>' output += _( - 'In reply to %(user)s %(post)s of %(date)s<br/>' + '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 += parent_post.format_for_email(quote_level = quote_level) + output += '</p>' + """ + output += parent_post.format_for_email( + quote_level = quote_level, + format = 'parent_subthread' + ) 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 '<p>%s</p>' % 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 += '<p>%s</p>' % 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 @@ -1055,10 +1019,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(): @@ -1677,11 +1638,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) @@ -1784,7 +1749,8 @@ 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 + #todo: move this to the askbot.mail module + from askbot.mail import send_mail email_context = { 'site': askbot_settings.APP_SHORT_NAME } @@ -1824,6 +1790,33 @@ 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 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/question.py b/askbot/models/question.py index a18e719b..1729b531 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( @@ -349,6 +350,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' @@ -772,6 +787,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/reply_by_email.py b/askbot/models/reply_by_email.py index 4398f693..0db7244c 100644 --- a/askbot/models/reply_by_email.py +++ b/askbot/models/reply_by_email.py @@ -4,11 +4,11 @@ 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 -from askbot.utils import mail +from askbot import mail class ReplyAddressManager(BaseQuerySetManager): """A manager for the :class:`ReplyAddress` model""" @@ -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')), ) @@ -74,36 +76,69 @@ class ReplyAddress(models.Model): """True if was used""" return self.used_at != None - def edit_post(self, parts): + 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, title = None, edit_response = False + ): """edits the created post upon repeated response to the same address""" - assert self.was_used == True - content, stored_files = mail.process_parts(parts) - self.user.edit_post( - post = self.response_post, - body_text = content, - revision_comment = _('edited by email'), - by_email = True - ) - self.response_post.thread.invalidate_cached_data() + if self.was_used or edit_response: + reply_action = 'append_content' + else: + reply_action = self.reply_action + + if edit_response: + post = self.response_post + else: + post = self.post - def create_reply(self, parts): + if 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 post.post_type == 'question': + assert(post is self.post) + self.user.edit_question( + question = post, + body_text = body_text, + title = title, + revision_comment = revision_comment, + by_email = True + ) + else: + self.user.edit_post( + post = post, + body_text = body_text, + revision_comment = revision_comment, + by_email = True + ) + self.post.thread.invalidate_cached_data() + + def create_reply(self, body_text): """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.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: @@ -114,13 +149,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: @@ -131,7 +166,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/models/signals.py b/askbot/models/signals.py index 28fe70b0..d538de76 100644 --- a/askbot/models/signals.py +++ b/askbot/models/signals.py @@ -32,6 +32,12 @@ post_updated = django.dispatch.Signal( 'newly_mentioned_users' ] ) +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/models/tag.py b/askbot/models/tag.py index 805f2174..0c3af52f 100644 --- a/askbot/models/tag.py +++ b/askbot/models/tag.py @@ -135,7 +135,9 @@ class GroupTagManager(BaseQuerySetManager): #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/skins/common/media/js/post.js b/askbot/skins/common/media/js/post.js index f867eed5..7a21a46b 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 641de201..4e0f91f1 100644 --- a/askbot/skins/common/media/js/utils.js +++ b/askbot/skins/common/media/js/utils.js @@ -888,8 +888,11 @@ SelectBoxItem.prototype.createDom = function() { elem.data('itemOriginalTitle', this._description); } -SelectBoxItem.prototype.decorate = function() { - throw "not implemented"; +SelectBoxItem.prototype.decorate = function(element) { + this._element = element; + this._id = element.data('itemId'); + this._name = element.html(); + this._description = element.data('originalTitle'); }; /** @@ -1015,10 +1018,12 @@ SelectBox.prototype.getSelectHandler = function(item) { SelectBox.prototype.decorate = function(element){ this._element = element; var me = this; - var items = this._element.find('.select-box-item'); - items.each(function(idx, item_element){ - var item = this.createItem(); + var box_items = this._items; + var item_elements = this._element.find('.select-box-item'); + item_elements.each(function(idx, item_element){ + var item = me.createItem(); item.decorate($(item_element)); + box_items.push(item); setupButtonEventHandlers( item.getElement(), me.getSelectHandler(item) @@ -1112,7 +1117,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/media/style/style.less b/askbot/skins/default/media/style/style.less index 5b33e044..02635b7c 100644 --- a/askbot/skins/default/media/style/style.less +++ b/askbot/skins/default/media/style/style.less @@ -1748,7 +1748,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) @@ -3183,6 +3186,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 { 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..e4449433 --- /dev/null +++ b/askbot/skins/default/templates/email/ask_for_signature.html @@ -0,0 +1,11 @@ +{% import "email/macros.html" as macros %} +<p style="{{ macros.heading_style() }}"> + {% trans %}{{ username }}, please reply to this message.{% endtrans %} +</p> +<p> + {% trans %}Your post could not be published, because we could not detect signature in your email.{% endtrans %}<br/> + {% trans %}Please make a simple response, without editing this message.{% endtrans %}<br/> + {% trans %}We will then attempt to detect the signature in your response and you should be able to post.{% endtrans %} +</p> +{% include "email/footer.html" %} +<p style="{{ macros.fine_print_style() }}">{{ footer_code }}</p> 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..da4c93ca --- /dev/null +++ b/askbot/skins/default/templates/email/insufficient_rep_to_post_by_email.html @@ -0,0 +1,15 @@ +{% import "email/macros.html" as macros %} +{# parameters: + * min_upvotes + * username + * site_name - for the footer + * site_link - html for the link +#} +<p style="{{ macros.heading_style() }}"> + {% trans %}{{ username }}, your question could not be posted by email just yet.{% endtrans %} +</p> +<p> + {% trans %}To make posts by email, you need to receive about {{min_upvotes}} upvotes.{% endtrans %}<br/> + {% trans link=site_link|safe %}At this time, please post your question at {{link}}{% endtrans %} +</p> +{% include "email/footer.html" %} diff --git a/askbot/skins/default/templates/email/macros.html b/askbot/skins/default/templates/email/macros.html new file mode 100644 index 00000000..1acbf515 --- /dev/null +++ b/askbot/skins/default/templates/email/macros.html @@ -0,0 +1,96 @@ +{% macro quoted_post( + post = None, + quote_level = 0, + format = None, + is_leaf_post = False + ) +%} + {% spaceless %} + {{ start_quote(quote_level) }} + {% set author = post.author.username %} + {% if post.post_type == 'question' %} + <p> + {% if format == 'parent_subthread' %} + {% if is_leaf_post %} + {% trans %}Question by {{ author }}:{% endtrans %} + {% else %} + {% trans -%} + In reply to {{ author }}'s question: + {%- endtrans %} + {% endif %} + {% else %} + {% trans %}Question :{% endtrans %} + {% endif %} + {{ post.thread.title }} + </p> + <p> + {% if format != 'parent_subthread' %} + {% trans %}Asked by {{ author }}:{% endtrans %} + {% endif %} + </p> + {% set tag_names = post.get_tag_names() %} + {% if tag_names %} + <p> + {% trans %}Tags:{% endtrans %} + {{ tag_names|join(', ') }}. + </p> + {% endif %} + {% elif post.post_type == 'answer' %} + <p> + {% 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 %} + </p> + {% else %} + <p> + {% 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 %} + </p> + {% endif %} + {{ post.html }} + {{ end_quote(quote_level) }} + {% endspaceless %} +{% endmacro %} + +{% macro start_quote(level = 0) %} + {% for number in range(level) %} + <div style="padding-left:5px; border-left: 2px solid #aaa;"> + {% endfor %} +{% endmacro %} + +{% macro end_quote(level = 0) %} + {% for number in range(level) %} + </div> + {% endfor %} +{% endmacro %} + +{% 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/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..085141d9 --- /dev/null +++ b/askbot/skins/default/templates/email/notify_author_about_approved_post.html @@ -0,0 +1,21 @@ +{# + 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 }} +<p>{% trans + 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 +<a href="mailto:{{ replace_content_address }}?body={{ post_text }}{{ author_email_signature}}&subject={{ subject }}">click here</a>{% endtrans %}</p> +<p>{% trans %}Below is a copy of your post:{% endtrans %}</p> +{% if post.post_type == 'question' %} + <p style="font-size:16px">{{ post.thread.title }}</p> +{% endif %} +{{ post.html }} +<p style="font-size:8px;color:#aaa;">{{ reply_code }}</p> 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 %} + <p> + {% trans count=comments|length -%} + {{ comment }} comment: + {%- pluralize -%} + {{ count }} comments: + {%- endtrans -%} + </p> + {% 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 new file mode 100644 index 00000000..ecc20ad9 --- /dev/null +++ b/askbot/skins/default/templates/email/quoted_post.html @@ -0,0 +1,5 @@ +{% from "email/macros.html" import quoted_post %} +{{ quoted_post( + post, quote_level, is_leaf_post = is_leaf_post, format = format + ) +}} diff --git a/askbot/skins/default/templates/email/welcome_lamson_on.html b/askbot/skins/default/templates/email/welcome_lamson_on.html index bcca4234..0efa7096 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 #} -<p style="font-size:16px;font-weight:bold;"> +<p style="{{ macros.heading_style() }}"> {% trans %}Welcome to {{ site_name }}!{% endtrans %} </p> <p> @@ -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 %} </p> {% include "email/footer.html" %} -<p style="color:#aaa;font-size:8px">{{ email_code }}</p>{# important #} +<p style="{{ macros.fine_print_style() }}">{{ email_code }}</p>{# important #} diff --git a/askbot/skins/default/templates/macros.html b/askbot/skins/default/templates/macros.html index 56c6e735..fcb75944 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 ) -%} <ul {% if id %}id="{{ id }}"{% endif %} @@ -214,7 +215,8 @@ poor design of the data or methods on data objects #} deletable = deletable, is_link = make_links, search_state = search_state, - html_tag = tag_html_tag + html_tag = tag_html_tag, + truncate_long_tag = False )}} {% endfor %} {% endif %} @@ -276,7 +278,8 @@ poor design of the data or methods on data objects #} css_class = None, search_state = None, html_tag = 'div', - extra_content = '' + extra_content = '', + truncate_long_tag = False ) -%} {% if not search_state %} {# get empty SearchState() if there's none; CAUTION: for some reason this doesn't work inside `spaceless` tag below! #} @@ -291,7 +294,12 @@ poor design of the data or methods on data objects #} title="{% trans %}see questions tagged '{{ tag }}'{% endtrans %}" {% endif %} rel="tag" - >{{ tag|replace('*', '✽')|truncate(20,True)}}</{% if not is_link or tag[-1] == '*' %}span{% else %}a{% endif %}> + data-tag-name="{{ tag|replace('*', '✽')|escape }}" + >{% if truncate_long_tag -%} + {{ tag|replace('*', '✽')|truncate(17, True) }} + {%- else -%} + {{ tag|replace('*', '✽') }} + {%- endif %}</{% if not is_link or tag[-1] == '*' %}span{% else %}a{% endif %}> {% if deletable %} <div class="delete-icon" {% if delete_link_title %} @@ -608,7 +616,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()) %} <a href="{{user.website}}" title="{% trans username=user.username|escape, url=user.website %}{{username}}'s website is {{url}}{% endtrans %}" diff --git a/askbot/skins/default/templates/question.html b/askbot/skins/default/templates/question.html index f4f36586..5403a644 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 %}' @@ -156,6 +157,9 @@ </script> {% endblock %} {% block content %} + <div> + {{ settings.QUESTION_PAGE_TOP_BANNER }} + </div> {% if is_cacheable %} {% cache long_time "thread-content-html" thread.id %} {% include "question/content.html" %} 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 #} - <input type="button" class="submit after-editor" id="fmanswer_button" value="{% trans %}Answer Your Own Question{% endtrans %}"/> + <input type="button" class="submit after-editor answer-own-question" id="fmanswer_button" value="{% trans %}Answer Your Own Question{% endtrans %}"/> {%endif%} diff --git a/askbot/skins/default/templates/tags.html b/askbot/skins/default/templates/tags.html index 6b89bdba..007388af 100644 --- a/askbot/skins/default/templates/tags.html +++ b/askbot/skins/default/templates/tags.html @@ -40,6 +40,7 @@ {{ macros.tag_widget( tag = tag.name, html_tag = 'div', + truncate_long_tag = True, extra_content = '<span class="tag-number">× ' ~ tag.used_count|intcomma ~ '</span>' ) 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/reject_post_dialog.html b/askbot/skins/default/templates/user_profile/reject_post_dialog.html index 987c511a..3483e83e 100644 --- a/askbot/skins/default/templates/user_profile/reject_post_dialog.html +++ b/askbot/skins/default/templates/user_profile/reject_post_dialog.html @@ -44,6 +44,7 @@ <ul class="select-box"> {% for reason in post_reject_reasons %} <li + class="select-box-item" data-original-title="{{reason.details.text|escape}}" data-item-id="{{reason.id}}" >{{reason.title|escape}}</li> diff --git a/askbot/skins/default/templates/user_profile/user.html b/askbot/skins/default/templates/user_profile/user.html index bb293b9b..fb40b206 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" %} <!-- user.html --> {% block title %}{% spaceless %}{{ page_title }}{% endspaceless %}{% endblock %} -{% block forestyle%} +{% block forestyle %} <style type="text/css"> .history-table td { padding: 5px; } </style> diff --git a/askbot/skins/default/templates/user_profile/user_info.html b/askbot/skins/default/templates/user_profile/user_info.html index 18e74464..ad460dbc 100644 --- a/askbot/skins/default/templates/user_profile/user_info.html +++ b/askbot/skins/default/templates/user_profile/user_info.html @@ -66,7 +66,7 @@ <td><strong title="{{ view_user.last_seen }}">{{ macros.timeago(view_user.last_seen) }}</strong></td> </tr> {% endif %} - {% if view_user.website %} + {% if view_user.website and (not view_user.is_blocked()) %} <tr> <td>{% trans %}website{% endtrans %}</td> <td>{{ macros.user_website_link(view_user, max_display_length = 30) }}</td> @@ -95,7 +95,7 @@ </td> <td width="380"> <div class="user-about"> - {% if view_user.about %} + {% if view_user.about and (not view_user.is_blocked()) %} {{view_user.about|linebreaks}} {% endif %} </div> 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/skins/default/templates/user_profile/user_stats.html b/askbot/skins/default/templates/user_profile/user_stats.html index b125589c..177df214 100644 --- a/askbot/skins/default/templates/user_profile/user_stats.html +++ b/askbot/skins/default/templates/user_profile/user_stats.html @@ -90,6 +90,7 @@ tag.name, html_tag = 'div', search_state = search_state, + truncate_long_tag = True, extra_content = '<span class="tag-number">× ' ~ tag.user_tag_usage_count|intcomma ~ 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" ><span>{% trans %}moderation{% endtrans %}</span></a> {% endif %} + {% if custom_tab_slug %} + <a id="{{ custom_tab_slug }}" {% if tab_name == custom_tab_slug %}class="on"{% endif %} + href="{% url user_profile view_user.id, view_user.username|slugify %}?sort={{ custom_tab_slug }}" + ><span>{{ custom_tab_name }}</span></a> + {% endif %} </div> </div> <div class="clean"></div> 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/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/tasks.py b/askbot/tasks.py index e5ba143d..da07477b 100644 --- a/askbot/tasks.py +++ b/askbot/tasks.py @@ -21,9 +21,13 @@ 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.badges import award_badges_signal @@ -32,6 +36,62 @@ 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_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: + #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, + '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, + '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 is now published') % 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( post_id, post_content_type_id, @@ -156,6 +216,7 @@ def record_post_update( post = post, recipients = notification_subscribers, ) + @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..3643e3c9 100644 --- a/askbot/templatetags/extra_filters_jinja.py +++ b/askbot/templatetags/extra_filters_jinja.py @@ -47,6 +47,13 @@ def add_tz_offset(datetime_object): return str(datetime_object) + ' ' + TIMEZONE_STR @register.filter +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): """removes path part of the url""" return url_utils.strip_path(url) 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') diff --git a/askbot/tests/email_alert_tests.py b/askbot/tests/email_alert_tests.py index abdb1c58..9ec1a412 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 @@ -949,3 +949,67 @@ 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): + 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) + 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' + ) + + def tearDown(self): + askbot_settings.update( + 'REPLY_BY_EMAIL', self.reply_by_email + ) + askbot_settings.update( + '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 + 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]) + + def test_moderated_question_answerable_approval_notification(self): + 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 + + +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) 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/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) diff --git a/askbot/tests/reply_by_email_tests.py b/askbot/tests/reply_by_email_tests.py index e46d6b3d..698662fc 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.mail.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/tests/utils.py b/askbot/tests/utils.py index fdeea371..4bc69ac4 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,16 +168,30 @@ class AskbotTestCase(TestCase): return user.post_answer( question = question, body_text = body_text, + by_email = by_email, follow = follow, wiki = wiki, 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, parent_post = None, body_text = 'test comment text', + by_email = False, timestamp = None ): """posts and returns a comment to parent post, uses @@ -187,6 +204,7 @@ class AskbotTestCase(TestCase): comment = user.post_comment( parent_post = parent_post, body_text = body_text, + by_email = by_email, timestamp = timestamp, ) 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() 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 '<a href="%s">%s</a>' % (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. diff --git a/askbot/views/commands.py b/askbot/views/commands.py index a1dc5420..79c266c2 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 @@ -499,6 +499,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') 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..9d9419e1 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 @@ -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) @@ -57,6 +58,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.exclude(status = 'b') group = None group_email_moderation_enabled = False user_can_join_group = False @@ -591,12 +593,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) @@ -661,7 +664,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, @@ -847,6 +851,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, @@ -858,6 +880,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 @@ -877,8 +905,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 @@ -904,6 +935,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 |