summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEvgeny Fadeev <evgeny.fadeev@gmail.com>2012-06-07 01:53:53 -0400
committerEvgeny Fadeev <evgeny.fadeev@gmail.com>2012-06-07 01:53:53 -0400
commit9f679e83639c642d903eeacde695baef1be44ec1 (patch)
treedd6c5772f77ac43c5044989959c6df678d16408b
parent428c91d505a4a09192f1bbc70f7f1c0c1429cc4b (diff)
parent84e1fc6f99879ede88f2d51df2b2730b5df6ff41 (diff)
downloadaskbot-9f679e83639c642d903eeacde695baef1be44ec1.tar.gz
askbot-9f679e83639c642d903eeacde695baef1be44ec1.tar.bz2
askbot-9f679e83639c642d903eeacde695baef1be44ec1.zip
made moderation dialog work again after the merge
-rw-r--r--askbot/conf/email.py30
-rw-r--r--askbot/conf/sidebar_question.py20
-rw-r--r--askbot/const/__init__.py44
-rw-r--r--askbot/doc/source/changelog.rst5
-rw-r--r--askbot/doc/source/management-commands.rst8
-rw-r--r--askbot/doc/source/optional-modules.rst45
-rw-r--r--askbot/forms.py2
-rw-r--r--askbot/mail/__init__.py (renamed from askbot/utils/mail.py)74
-rw-r--r--askbot/mail/lamson_handlers.py (renamed from askbot/lamson_handlers.py)94
-rw-r--r--askbot/mail/messages.py52
-rw-r--r--askbot/management/commands/fix_answer_counts.py5
-rw-r--r--askbot/management/commands/fix_question_tags.py30
-rw-r--r--askbot/management/commands/fix_revisionless_posts.py7
-rw-r--r--askbot/management/commands/post_emailed_questions.py2
-rw-r--r--askbot/management/commands/send_accept_answer_reminders.py2
-rw-r--r--askbot/management/commands/send_email.py2
-rw-r--r--askbot/management/commands/send_email_alerts.py2
-rw-r--r--askbot/management/commands/send_respondable_welcome_email.py12
-rw-r--r--askbot/management/commands/send_unanswered_question_reminders.py2
-rw-r--r--askbot/models/__init__.py228
-rw-r--r--askbot/models/post.py181
-rw-r--r--askbot/models/question.py16
-rw-r--r--askbot/models/reply_by_email.py77
-rw-r--r--askbot/models/signals.py6
-rw-r--r--askbot/models/tag.py4
-rw-r--r--askbot/skins/common/media/js/post.js5
-rw-r--r--askbot/skins/common/media/js/utils.js19
-rw-r--r--askbot/skins/default/media/style/style.less10
-rw-r--r--askbot/skins/default/templates/email/ask_for_signature.html11
-rw-r--r--askbot/skins/default/templates/email/insufficient_rep_to_post_by_email.html15
-rw-r--r--askbot/skins/default/templates/email/macros.html96
-rw-r--r--askbot/skins/default/templates/email/notify_author_about_approved_post.html21
-rw-r--r--askbot/skins/default/templates/email/post_as_subthread.html17
-rw-r--r--askbot/skins/default/templates/email/quoted_post.html5
-rw-r--r--askbot/skins/default/templates/email/welcome_lamson_on.html5
-rw-r--r--askbot/skins/default/templates/macros.html18
-rw-r--r--askbot/skins/default/templates/question.html4
-rw-r--r--askbot/skins/default/templates/question/content.html2
-rw-r--r--askbot/skins/default/templates/tags.html1
-rw-r--r--askbot/skins/default/templates/user_profile/custom_tab.html3
-rw-r--r--askbot/skins/default/templates/user_profile/reject_post_dialog.html1
-rw-r--r--askbot/skins/default/templates/user_profile/user.html2
-rw-r--r--askbot/skins/default/templates/user_profile/user_info.html4
-rw-r--r--askbot/skins/default/templates/user_profile/user_moderate.html2
-rw-r--r--askbot/skins/default/templates/user_profile/user_stats.html1
-rw-r--r--askbot/skins/default/templates/user_profile/user_tabs.html5
-rw-r--r--askbot/skins/loaders.py1
-rw-r--r--askbot/startup_procedures.py32
-rw-r--r--askbot/tasks.py63
-rw-r--r--askbot/templatetags/extra_filters_jinja.py7
-rw-r--r--askbot/tests/db_api_tests.py17
-rw-r--r--askbot/tests/email_alert_tests.py66
-rw-r--r--askbot/tests/page_load_tests.py19
-rw-r--r--askbot/tests/post_model_tests.py2
-rw-r--r--askbot/tests/reply_by_email_tests.py113
-rw-r--r--askbot/tests/utils.py20
-rw-r--r--askbot/utils/email.py28
-rw-r--r--askbot/utils/html.py15
-rw-r--r--askbot/views/commands.py5
-rw-r--r--askbot/views/meta.py2
-rw-r--r--askbot/views/users.py54
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('*', '&#10045;')|truncate(20,True)}}</{% if not is_link or tag[-1] == '*' %}span{% else %}a{% endif %}>
+ data-tag-name="{{ tag|replace('*', '&#10045;')|escape }}"
+ >{% if truncate_long_tag -%}
+ {{ tag|replace('*', '&#10045;')|truncate(17, True) }}
+ {%- else -%}
+ {{ tag|replace('*', '&#10045;') }}
+ {%- 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">&#215; ' ~
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">&#215; ' ~
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 `&lt;` was `&amp;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('&lt;&lt;&lt;tag1&gt;&gt;&gt; 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