summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEvgeny Fadeev <evgeny.fadeev@gmail.com>2012-11-01 01:03:47 -0300
committerEvgeny Fadeev <evgeny.fadeev@gmail.com>2012-11-01 01:03:47 -0300
commita8ebdd9c1e5c76a7a9c7ad77ddda74eaa2d79ec4 (patch)
tree946de5143cadb4d85e7ec896c85fd03af68078cc
parent813f17dea5e1084ebc98b096b965a06ca6ea56d2 (diff)
downloadaskbot-a8ebdd9c1e5c76a7a9c7ad77ddda74eaa2d79ec4.tar.gz
askbot-a8ebdd9c1e5c76a7a9c7ad77ddda74eaa2d79ec4.tar.bz2
askbot-a8ebdd9c1e5c76a7a9c7ad77ddda74eaa2d79ec4.zip
added lower rep barrier to accept any posts with links - as a first line of defense against spam
-rw-r--r--askbot/conf/minimum_reputation.py15
-rw-r--r--askbot/doc/source/changelog.rst1
-rw-r--r--askbot/forms.py54
-rw-r--r--askbot/forms.py.orig1629
-rw-r--r--askbot/mail/__init__.py4
-rw-r--r--askbot/mail/__init__.py.orig431
-rw-r--r--askbot/models/__init__.py4
-rw-r--r--askbot/models/post.py11
-rw-r--r--askbot/models/question.py5
-rw-r--r--askbot/tests/form_tests.py56
-rw-r--r--askbot/views/commands.py2
-rw-r--r--askbot/views/readers.py2
-rw-r--r--askbot/views/widgets.py12
-rw-r--r--askbot/views/writers.py45
14 files changed, 2205 insertions, 66 deletions
diff --git a/askbot/conf/minimum_reputation.py b/askbot/conf/minimum_reputation.py
index 359855c9..fee880ac 100644
--- a/askbot/conf/minimum_reputation.py
+++ b/askbot/conf/minimum_reputation.py
@@ -108,8 +108,21 @@ settings.register(
livesettings.IntegerValue(
MIN_REP,
'MIN_REP_TO_INSERT_LINK',
+ default=30,
+ description=_('Insert clickable links')
+ )
+)
+
+settings.register(
+ livesettings.IntegerValue(
+ MIN_REP,
+ 'MIN_REP_TO_SUGGEST_LINK',
default=10,
- description=_('Insert links')
+ description=_('Insert link suggestions as plain text'),
+ help_text=_(
+ 'This value should be smaller than that for "insert clickable links". '
+ 'This setting should stop link-spamming by newly registered users.'
+ )
)
)
diff --git a/askbot/doc/source/changelog.rst b/askbot/doc/source/changelog.rst
index fcc85c21..f5714421 100644
--- a/askbot/doc/source/changelog.rst
+++ b/askbot/doc/source/changelog.rst
@@ -4,6 +4,7 @@ Changes in Askbot
Development version
-------------------
* Added minimum reputation to insert links and hotlinked images (Evgeny)
+* Added minimum reputation to suggest links as plain text (Evgeny)
* Added support of Haystack for search (Adolfo)
* Added minimum reputation setting to accept any answer as correct (Evgeny)
* Added "VIP" option to groups - if checked, all posts belong to the group and users of that group in the future will be able to moderate those posts. Moderation features for VIP group are in progress (Evgeny)
diff --git a/askbot/forms.py b/askbot/forms.py
index d1f4a31b..a4365776 100644
--- a/askbot/forms.py
+++ b/askbot/forms.py
@@ -11,6 +11,7 @@ from django.utils.text import get_text_list
from django.contrib.auth.models import User
from django_countries import countries
from askbot.utils.forms import NextUrlField, UserNameField
+from askbot.utils.markup import URL_RE
from askbot.mail import extract_first_email_address
from recaptcha_works.fields import RecaptchaField
from askbot.conf import settings as askbot_settings
@@ -273,6 +274,11 @@ class EditorField(forms.CharField):
min_length = 10 # sentinel default value
def __init__(self, *args, **kwargs):
+ user = kwargs.pop('user', None)
+ if user is None:
+ raise ValueError('user parameter is required')
+ self.user = user
+
editor_attrs = kwargs.pop('editor_attrs', {})
super(EditorField, self).__init__(*args, **kwargs)
self.required = True
@@ -295,6 +301,21 @@ class EditorField(forms.CharField):
self.min_length
) % self.min_length
raise forms.ValidationError(msg)
+
+ if re.search(URL_RE, value):
+ min_rep = askbot_settings.MIN_REP_TO_SUGGEST_LINK
+ if self.user.is_anonymous():
+ raise forms.ValidationError(
+ _('Links or images cannot be posted anonymously')
+ )
+ elif self.user.reputation < min_rep:
+ raise forms.ValidationError(
+ ungettext_lazy(
+ 'At at least %d karma point is required to post links',
+ 'At at least %d karma points are required to post links',
+ min_rep
+ ) % min_rep
+ )
return value
@@ -302,7 +323,10 @@ class QuestionEditorField(EditorField):
"""Editor field for the questions"""
def __init__(self, *args, **kwargs):
- super(QuestionEditorField, self).__init__(*args, **kwargs)
+ user = kwargs.pop('user', None)
+ super(QuestionEditorField, self).__init__(
+ user=user, *args, **kwargs
+ )
self.length_error_template_singular = \
'question body must be > %d character'
self.length_error_template_plural = \
@@ -477,10 +501,12 @@ class EditorForm(forms.Form):
the field must be created dynamically, so it's added
in the __init__() function"""
- def __init__(self, editor_attrs=None):
+ def __init__(self, user=None, editor_attrs=None):
super(EditorForm, self).__init__()
editor_attrs = editor_attrs or {}
- self.fields['editor'] = EditorField(editor_attrs=editor_attrs)
+ self.fields['editor'] = EditorField(
+ user=user, editor_attrs=editor_attrs
+ )
class DumpUploadForm(forms.Form):
@@ -898,9 +924,10 @@ class AskForm(PostAsSomeoneForm, PostPrivatelyForm):
)
def __init__(self, *args, **kwargs):
+ user = kwargs.pop('user', None)
super(AskForm, self).__init__(*args, **kwargs)
#it's important that this field is set up dynamically
- self.fields['text'] = QuestionEditorField()
+ self.fields['text'] = QuestionEditorField(user=user)
#hide ask_anonymously field
if askbot_settings.ALLOW_ASK_ANONYMOUSLY is False:
self.hide_field('ask_anonymously')
@@ -933,11 +960,12 @@ class AskWidgetForm(forms.Form, FormWithHideableFields):
)
def __init__(self, include_text=True, *args, **kwargs):
+ user = kwargs.pop('user', None)
super(AskWidgetForm, self).__init__(*args, **kwargs)
#hide ask_anonymously field
if not askbot_settings.ALLOW_ASK_ANONYMOUSLY:
self.hide_field('ask_anonymously')
- self.fields['text'] = QuestionEditorField()
+ self.fields['text'] = QuestionEditorField(user=user)
if not include_text:
self.hide_field('text')
#hack to make it validate
@@ -1021,7 +1049,11 @@ class AskByEmailForm(forms.Form):
'required': ASK_BY_EMAIL_SUBJECT_HELP
}
)
- body_text = QuestionEditorField()
+
+ def __init__(self, *args, **kwargs):
+ user = kwargs.pop('user', None)
+ super(AskByEmailForm, self).__init__(*args, **kwargs)
+ self.fields['body_text'] = QuestionEditorField(user=user)
def clean_sender(self):
"""Cleans the :attr:`~askbot.forms.AskByEmail.sender` attribute
@@ -1075,7 +1107,6 @@ class AskByEmailForm(forms.Form):
class AnswerForm(PostAsSomeoneForm, PostPrivatelyForm):
- text = AnswerEditorField()
wiki = WikiField()
openid = forms.CharField(
required=False, max_length=255,
@@ -1085,7 +1116,7 @@ class AnswerForm(PostAsSomeoneForm, PostPrivatelyForm):
def __init__(self, *args, **kwargs):
super(AnswerForm, self).__init__(*args, **kwargs)
- self.fields['text'] = AnswerEditorField()
+ self.fields['text'] = AnswerEditorField(user=kwargs['user'])
self.fields['email_notify'].widget.attrs['id'] = \
'question-subscribe-updates'
@@ -1170,11 +1201,11 @@ class EditQuestionForm(PostAsSomeoneForm, PostPrivatelyForm):
def __init__(self, *args, **kwargs):
"""populate EditQuestionForm with initial data"""
self.question = kwargs.pop('question')
- self.user = kwargs['user']#preserve for superclass
+ self.user = kwargs.pop('user')#preserve for superclass
revision = kwargs.pop('revision')
super(EditQuestionForm, self).__init__(*args, **kwargs)
#it is important to add this field dynamically
- self.fields['text'] = QuestionEditorField()
+ self.fields['text'] = QuestionEditorField(user=self.user)
self.fields['title'].initial = revision.title
self.fields['text'].initial = revision.text
self.fields['tags'].initial = revision.tagnames
@@ -1276,9 +1307,10 @@ class EditAnswerForm(PostAsSomeoneForm, PostPrivatelyForm):
def __init__(self, answer, revision, *args, **kwargs):
self.answer = answer
+ user = kwargs.pop('user', None)
super(EditAnswerForm, self).__init__(*args, **kwargs)
#it is important to add this field dynamically
- self.fields['text'] = AnswerEditorField()
+ self.fields['text'] = AnswerEditorField(user=user)
self.fields['text'].initial = revision.text
self.fields['wiki'].initial = answer.wiki
diff --git a/askbot/forms.py.orig b/askbot/forms.py.orig
new file mode 100644
index 00000000..d1f4a31b
--- /dev/null
+++ b/askbot/forms.py.orig
@@ -0,0 +1,1629 @@
+"""Forms, custom form fields and related utility functions
+used in AskBot"""
+import re
+from django import forms
+from askbot import const
+from askbot.const import message_keys
+from django.forms.util import ErrorList
+from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import ungettext_lazy, string_concat
+from django.utils.text import get_text_list
+from django.contrib.auth.models import User
+from django_countries import countries
+from askbot.utils.forms import NextUrlField, UserNameField
+from askbot.mail import extract_first_email_address
+from recaptcha_works.fields import RecaptchaField
+from askbot.conf import settings as askbot_settings
+from askbot.conf import get_tag_display_filter_strategy_choices
+from tinymce.widgets import TinyMCE
+import logging
+
+
+def cleanup_dict(dictionary, key, empty_value):
+ """deletes key from dictionary if it exists
+ and the corresponding value equals the empty_value
+ """
+ if key in dictionary and dictionary[key] == empty_value:
+ del dictionary[key]
+
+
+def format_form_errors(form):
+ """Formats form errors in HTML
+ if there is only one error - returns a plain string
+ if more than one, returns an unordered list of errors
+ in HTML format.
+ If there are no errors, returns empty string
+ """
+ if form.errors:
+ errors = form.errors.values()
+ if len(errors) == 1:
+ return errors[0]
+ else:
+ result = '<ul>'
+ for error in errors:
+ result += '<li>%s</li>' % error
+ result += '</ul>'
+ return result
+ else:
+ return ''
+
+
+def clean_marked_tagnames(tagnames):
+ """return two strings - one containing tagnames
+ that are straight names of tags, and the second one
+ containing names of wildcard tags,
+ wildcard tags are those that have an asterisk at the end
+ the function does not verify that the tag names are valid
+ """
+ if askbot_settings.USE_WILDCARD_TAGS is False:
+ return tagnames, list()
+
+ pure_tags = list()
+ wildcards = list()
+ for tagname in tagnames:
+ if tagname == '':
+ continue
+ if tagname.endswith('*'):
+ if tagname.count('*') > 1:
+ continue
+ else:
+ wildcards.append(tagname)
+ else:
+ pure_tags.append(tagname)
+
+ return pure_tags, wildcards
+
+
+def filter_choices(remove_choices=None, from_choices=None):
+ """a utility function that will remove choice tuples
+ usable for the forms.ChoicesField from
+ ``from_choices``, the removed ones will be those given
+ by the ``remove_choice`` list
+
+ there is no error checking, ``from_choices`` tuple must be as expected
+ to work with the forms.ChoicesField
+ """
+
+ if not isinstance(remove_choices, list):
+ raise TypeError('remove_choices must be a list')
+
+ filtered_choices = tuple()
+ for choice_to_test in from_choices:
+ remove = False
+ for choice in remove_choices:
+ if choice == choice_to_test[0]:
+ remove = True
+ break
+ if remove is False:
+ filtered_choices += (choice_to_test, )
+
+ return filtered_choices
+
+
+def need_mandatory_tags():
+ """true, if list of mandatory tags is not empty"""
+ from askbot import models
+ return (
+ askbot_settings.TAGS_ARE_REQUIRED
+ and len(models.tag.get_mandatory_tags()) > 0
+ )
+
+
+def mandatory_tag_missing_in_list(tag_strings):
+ """true, if mandatory tag is not present in the list
+ of ``tag_strings``"""
+ from askbot import models
+ mandatory_tags = models.tag.get_mandatory_tags()
+ for mandatory_tag in mandatory_tags:
+ for tag_string in tag_strings:
+ if tag_strings_match(tag_string, mandatory_tag):
+ return False
+ return True
+
+
+def tag_strings_match(tag_string, mandatory_tag):
+ """true if tag string matches the mandatory tag,
+ the comparison is not symmetric if tag_string ends with a
+ wildcard (asterisk)
+ """
+ if mandatory_tag.endswith('*'):
+ return tag_string.startswith(mandatory_tag[:-1])
+ else:
+ return tag_string == mandatory_tag
+
+
+
+COUNTRY_CHOICES = (('unknown', _('select country')),) + countries.COUNTRIES
+
+
+class CountryField(forms.ChoiceField):
+ """this is better placed into the django_coutries app"""
+
+ def __init__(self, *args, **kwargs):
+ """sets label and the country choices
+ """
+ kwargs['choices'] = kwargs.pop('choices', COUNTRY_CHOICES)
+ kwargs['label'] = kwargs.pop('label', _('Country'))
+ super(CountryField, self).__init__(*args, **kwargs)
+
+ def clean(self, value):
+ """Handles case of 'unknown' country selection
+ """
+ if self.required:
+ if value == 'unknown':
+ raise forms.ValidationError(_('Country field is required'))
+ if value == 'unknown':
+ return None
+ return value
+
+
+class CountedWordsField(forms.CharField):
+ """a field where a number of words is expected
+ to be in a certain range"""
+
+ def __init__(
+ self, min_words=0, max_words=9999, field_name=None,
+ *args, **kwargs
+ ):
+ self.min_words = min_words
+ self.max_words = max_words
+ self.field_name = field_name
+ super(CountedWordsField, self).__init__(*args, **kwargs)
+
+ def clean(self, value):
+ #todo: this field must be adapted to work with Chinese, etc.
+ #for that we'll have to count characters instead of words
+ if value is None:
+ value = ''
+
+ value = value.strip()
+
+ word_count = len(value.split())
+ if word_count < self.min_words:
+ msg = ungettext_lazy(
+ 'must be > %d word',
+ 'must be > %d words',
+ self.min_words - 1
+ ) % (self.min_words - 1)
+ #todo - space is not used in Chinese
+ raise forms.ValidationError(
+ string_concat(self.field_name, ' ', msg)
+ )
+
+ if word_count > self.max_words:
+ msg = ungettext_lazy(
+ 'must be < %d word',
+ 'must be < %d words',
+ self.max_words + 1
+ ) % (self.max_words + 1)
+ raise forms.ValidationError(
+ string_concat(self.field_name, ' ', msg)
+ )
+ return value
+
+
+class DomainNameField(forms.CharField):
+ """Field for Internet Domain Names
+ todo: maybe there is a standard field for this?
+ """
+ def clean(self, value):
+ #find a better regex, taking into account tlds
+ domain_re = re.compile(r'[a-zA-Z\d]+(\.[a-zA-Z\d]+)+')
+ if domain_re.match(value):
+ return value
+ else:
+ raise forms.ValidationError(
+ '%s is not a valid domain name' % value
+ )
+
+
+class TitleField(forms.CharField):
+ """Fild receiving question title"""
+ def __init__(self, *args, **kwargs):
+ super(TitleField, self).__init__(*args, **kwargs)
+ self.required = kwargs.get('required', True)
+ self.widget = forms.TextInput(
+ attrs={'size': 70, 'autocomplete': 'off'}
+ )
+ self.max_length = 255
+ self.label = _('title')
+ self.help_text = _(
+ 'please enter a descriptive title for your question'
+ )
+ self.initial = ''
+
+ def clean(self, value):
+ """cleans the field for minimum and maximum length
+ also is supposed to work for unicode non-ascii characters"""
+ if value is None:
+ value = ''
+ if len(value) < askbot_settings.MIN_TITLE_LENGTH:
+ msg = ungettext_lazy(
+ 'title must be > %d character',
+ 'title must be > %d characters',
+ askbot_settings.MIN_TITLE_LENGTH
+ ) % askbot_settings.MIN_TITLE_LENGTH
+ raise forms.ValidationError(msg)
+ encoded_value = value.encode('utf-8')
+ if len(value) == len(encoded_value):
+ if len(value) > self.max_length:
+ raise forms.ValidationError(
+ _(
+ 'The title is too long, maximum allowed size is '
+ '%d characters'
+ ) % self.max_length
+ )
+ elif len(encoded_value) > self.max_length:
+ raise forms.ValidationError(
+ _(
+ 'The title is too long, maximum allowed size is '
+ '%d bytes'
+ ) % self.max_length
+ )
+
+ return value.strip() # TODO: test me
+
+
+class EditorField(forms.CharField):
+ """EditorField is subclassed by the
+ :class:`QuestionEditorField` and :class:`AnswerEditorField`
+ """
+ length_error_template_singular = 'post content must be > %d character',
+ length_error_template_plural = 'post content must be > %d characters',
+ min_length = 10 # sentinel default value
+
+ def __init__(self, *args, **kwargs):
+ editor_attrs = kwargs.pop('editor_attrs', {})
+ super(EditorField, self).__init__(*args, **kwargs)
+ self.required = True
+ widget_attrs = {'id': 'editor'}
+ if askbot_settings.EDITOR_TYPE == 'markdown':
+ self.widget = forms.Textarea(attrs=widget_attrs)
+ elif askbot_settings.EDITOR_TYPE == 'tinymce':
+ self.widget = TinyMCE(attrs=widget_attrs, mce_attrs=editor_attrs)
+ self.label = _('content')
+ self.help_text = u''
+ self.initial = ''
+
+ def clean(self, value):
+ if value is None:
+ value = ''
+ if len(value) < self.min_length:
+ msg = ungettext_lazy(
+ self.length_error_template_singular,
+ self.length_error_template_plural,
+ self.min_length
+ ) % self.min_length
+ raise forms.ValidationError(msg)
+ return value
+
+
+class QuestionEditorField(EditorField):
+ """Editor field for the questions"""
+
+ def __init__(self, *args, **kwargs):
+ super(QuestionEditorField, self).__init__(*args, **kwargs)
+ self.length_error_template_singular = \
+ 'question body must be > %d character'
+ self.length_error_template_plural = \
+ 'question body must be > %d characters'
+ self.min_length = askbot_settings.MIN_QUESTION_BODY_LENGTH
+
+
+class AnswerEditorField(EditorField):
+ """Editor field for answers"""
+
+ def __init__(self, *args, **kwargs):
+ super(AnswerEditorField, self).__init__(*args, **kwargs)
+ self.length_error_template_singular = 'answer must be > %d character'
+ self.length_error_template_plural = 'answer must be > %d characters'
+ self.min_length = askbot_settings.MIN_ANSWER_BODY_LENGTH
+
+
+def clean_tag(tag_name):
+ """a function that cleans a single tag name"""
+ tag_length = len(tag_name)
+ if tag_length > askbot_settings.MAX_TAG_LENGTH:
+ #singular form is odd in english, but required for pluralization
+ #in other languages
+ msg = ungettext_lazy(
+ #odd but added for completeness
+ 'each tag must be shorter than %(max_chars)d character',
+ 'each tag must be shorter than %(max_chars)d characters',
+ tag_length
+ ) % {'max_chars': tag_length}
+ raise forms.ValidationError(msg)
+
+ #todo - this needs to come from settings
+ tagname_re = re.compile(const.TAG_REGEX, re.UNICODE)
+ if not tagname_re.search(tag_name):
+ raise forms.ValidationError(
+ _(message_keys.TAG_WRONG_CHARS_MESSAGE)
+ )
+
+ if askbot_settings.FORCE_LOWERCASE_TAGS:
+ #a simpler way to handle tags - just lowercase thew all
+ return tag_name.lower()
+ else:
+ try:
+ from askbot import models
+ stored_tag = models.Tag.objects.get(name__iexact=tag_name)
+ return stored_tag.name
+ except models.Tag.DoesNotExist:
+ return tag_name
+
+
+class TagNamesField(forms.CharField):
+ """field that receives AskBot tag names"""
+
+ def __init__(self, *args, **kwargs):
+ super(TagNamesField, self).__init__(*args, **kwargs)
+ self.required = kwargs.get('required',
+ askbot_settings.TAGS_ARE_REQUIRED)
+ self.widget = forms.TextInput(
+ attrs={'size': 50, 'autocomplete': 'off'}
+ )
+ self.max_length = 255
+ self.error_messages['max_length'] = _(
+ 'We ran out of space for recording the tags. '
+ 'Please shorten or delete some of them.'
+ )
+ self.label = _('tags')
+ self.help_text = ungettext_lazy(
+ 'Tags are short keywords, with no spaces within. '
+ 'Up to %(max_tags)d tag can be used.',
+ 'Tags are short keywords, with no spaces within. '
+ 'Up to %(max_tags)d tags can be used.',
+ askbot_settings.MAX_TAGS_PER_POST
+ ) % {'max_tags': askbot_settings.MAX_TAGS_PER_POST}
+ self.initial = ''
+
+ def clean(self, value):
+ from askbot import models
+ value = super(TagNamesField, self).clean(value)
+ data = value.strip()
+ if len(data) < 1:
+ if askbot_settings.TAGS_ARE_REQUIRED:
+ raise forms.ValidationError(
+ _(message_keys.TAGS_ARE_REQUIRED_MESSAGE)
+ )
+ else:
+ #don't test for required characters when tags is ''
+ return ''
+ split_re = re.compile(const.TAG_SPLIT_REGEX)
+ tag_strings = split_re.split(data)
+ entered_tags = []
+ tag_count = len(tag_strings)
+ if tag_count > askbot_settings.MAX_TAGS_PER_POST:
+ max_tags = askbot_settings.MAX_TAGS_PER_POST
+ msg = ungettext_lazy(
+ 'please use %(tag_count)d tag or less',
+ 'please use %(tag_count)d tags or less',
+ tag_count) % {'tag_count': max_tags}
+ raise forms.ValidationError(msg)
+
+ if need_mandatory_tags():
+ if mandatory_tag_missing_in_list(tag_strings):
+ msg = _(
+ 'At least one of the following tags is required : %(tags)s'
+ ) % {'tags': get_text_list(models.tag.get_mandatory_tags())}
+ raise forms.ValidationError(msg)
+
+ cleaned_entered_tags = list()
+ for tag in tag_strings:
+ cleaned_tag = clean_tag(tag)
+ if cleaned_tag not in cleaned_entered_tags:
+ cleaned_entered_tags.append(clean_tag(tag))
+
+ result = u' '.join(cleaned_entered_tags)
+
+ if len(result) > 125:#magic number!, the same as max_length in db
+ raise forms.ValidationError(self.error_messages['max_length'])
+
+ return u' '.join(cleaned_entered_tags)
+
+
+class WikiField(forms.BooleanField):
+ """Rendered as checkbox turning post into
+ "community wiki"
+ """
+
+ def __init__(self, *args, **kwargs):
+ super(WikiField, self).__init__(*args, **kwargs)
+ self.required = False
+ self.initial = False
+ self.label = _(
+ 'community wiki (karma is not awarded & '
+ 'many others can edit wiki post)'
+ )
+ self.help_text = _(
+ 'if you choose community wiki option, the question '
+ 'and answer do not generate points and name of '
+ 'author will not be shown'
+ )
+
+ def clean(self, value):
+ return value and askbot_settings.WIKI_ON
+
+
+class EmailNotifyField(forms.BooleanField):
+ """Rendered as checkbox which turns on
+ email notifications on the post"""
+ def __init__(self, *args, **kwargs):
+ super(EmailNotifyField, self).__init__(*args, **kwargs)
+ self.required = False
+ self.widget.attrs['class'] = 'nomargin'
+
+
+class SummaryField(forms.CharField):
+
+ def __init__(self, *args, **kwargs):
+ super(SummaryField, self).__init__(*args, **kwargs)
+ self.required = False
+ self.widget = forms.TextInput(
+ attrs={'size': 50, 'autocomplete': 'off'}
+ )
+ self.max_length = 300
+ self.label = _('update summary:')
+ self.help_text = _(
+ 'enter a brief summary of your revision (e.g. '
+ 'fixed spelling, grammar, improved style, this '
+ 'field is optional)'
+ )
+
+
+class EditorForm(forms.Form):
+ """form with one field - `editor`
+ the field must be created dynamically, so it's added
+ in the __init__() function"""
+
+ def __init__(self, editor_attrs=None):
+ super(EditorForm, self).__init__()
+ editor_attrs = editor_attrs or {}
+ self.fields['editor'] = EditorField(editor_attrs=editor_attrs)
+
+
+class DumpUploadForm(forms.Form):
+ """This form handles importing
+ data into the forum. At the moment it only
+ supports stackexchange import.
+ """
+ dump_file = forms.FileField()
+
+
+class ShowQuestionForm(forms.Form):
+ """Cleans data necessary to access answers and comments
+ by the respective comment or answer id - necessary
+ when comments would be normally wrapped and/or displayed
+ on the page other than the first page of answers to a question.
+ Same for the answers that are shown on the later pages.
+ """
+ answer = forms.IntegerField(required=False)
+ comment = forms.IntegerField(required=False)
+ page = forms.IntegerField(required=False)
+ sort = forms.CharField(required=False)
+
+ def __init__(self, data, default_sort_method):
+ super(ShowQuestionForm, self).__init__(data)
+ self.default_sort_method = default_sort_method
+
+ def get_pruned_data(self):
+ nones = ('answer', 'comment', 'page')
+ for key in nones:
+ if key in self.cleaned_data:
+ if self.cleaned_data[key] is None:
+ del self.cleaned_data[key]
+ if 'sort' in self.cleaned_data:
+ if self.cleaned_data['sort'] == '':
+ del self.cleaned_data['sort']
+ return self.cleaned_data
+
+ def clean(self):
+ """this form must always be valid
+ should use defaults if the data is incomplete
+ or invalid"""
+ if self._errors:
+ #since the form is always valid, clear the errors
+ logging.error(unicode(self._errors))
+ self._errors = {}
+
+ in_data = self.get_pruned_data()
+ out_data = dict()
+ if ('answer' in in_data) ^ ('comment' in in_data):
+ out_data['show_page'] = None
+ out_data['answer_sort_method'] = 'votes'
+ out_data['show_comment'] = in_data.get('comment', None)
+ out_data['show_answer'] = in_data.get('answer', None)
+ else:
+ out_data['show_page'] = in_data.get('page', 1)
+ out_data['answer_sort_method'] = in_data.get(
+ 'sort',
+ self.default_sort_method
+ )
+ out_data['show_comment'] = None
+ out_data['show_answer'] = None
+ self.cleaned_data = out_data
+ return out_data
+
+
+class ChangeUserReputationForm(forms.Form):
+ """Form that allows moderators and site administrators
+ to adjust reputation of users.
+
+ this form internally verifies that user who claims to
+ be a moderator acually is
+ """
+
+ user_reputation_delta = forms.IntegerField(
+ min_value=1,
+ label=_(
+ 'Enter number of points to add or subtract'
+ )
+ )
+ comment = forms.CharField(max_length=128)
+
+ def clean_comment(self):
+ if 'comment' in self.cleaned_data:
+ comment = self.cleaned_data['comment'].strip()
+ if comment == '':
+ del self.cleaned_data['comment']
+ raise forms.ValidationError('Please enter non-empty comment')
+ self.cleaned_data['comment'] = comment
+ return comment
+
+MODERATOR_STATUS_CHOICES = (
+ ('a', _('approved')),
+ ('w', _('watched')),
+ ('s', _('suspended')),
+ ('b', _('blocked')),
+ )
+ADMINISTRATOR_STATUS_CHOICES = (('d', _('administrator')),
+ ('m', _('moderator')), ) \
+ + MODERATOR_STATUS_CHOICES
+
+
+class ChangeUserStatusForm(forms.Form):
+ """form that allows moderators to change user's status
+
+ the type of options displayed depend on whether user
+ is a moderator or a site administrator as well as
+ what is the current status of the moderated user
+
+ for example moderators cannot moderate other moderators
+ and admins. Admins can take away admin status, but cannot
+ add it (that can be done through the Django Admin interface
+
+ this form is to be displayed in the user profile under
+ "moderation" tab
+ """
+
+ user_status = forms.ChoiceField(label=_('Change status to'))
+
+ def __init__(self, *arg, **kwarg):
+
+ moderator = kwarg.pop('moderator')
+ subject = kwarg.pop('subject')
+
+ super(ChangeUserStatusForm, self).__init__(*arg, **kwarg)
+
+ #select user_status_choices depending on status of the moderator
+ if moderator.is_administrator():
+ user_status_choices = ADMINISTRATOR_STATUS_CHOICES
+ elif moderator.is_moderator():
+ user_status_choices = MODERATOR_STATUS_CHOICES
+ if subject.is_moderator() and subject != moderator:
+ raise ValueError('moderator cannot moderate another moderator')
+ else:
+ raise ValueError('moderator or admin expected from "moderator"')
+
+ #remove current status of the "subject" user from choices
+ user_status_choices = filter_choices(
+ remove_choices=[subject.status, ],
+ from_choices=user_status_choices
+ )
+
+ #add prompt option
+ user_status_choices = (('select', _('which one?')), ) \
+ + user_status_choices
+
+ self.fields['user_status'].choices = user_status_choices
+
+ #set prompt option as default
+ self.fields['user_status'].default = 'select'
+ self.moderator = moderator
+ self.subject = subject
+
+ def clean(self):
+ #if moderator is looking at own profile - do not
+ #let change status
+ if 'user_status' in self.cleaned_data:
+
+ user_status = self.cleaned_data['user_status']
+
+ #does not make sense to change own user status
+ #if necessary, this can be done from the Django admin interface
+ if self.moderator == self.subject:
+ del self.cleaned_data['user_status']
+ raise forms.ValidationError(_('Cannot change own status'))
+
+ #do not let moderators turn other users into moderators
+ if self.moderator.is_moderator() and user_status == 'moderator':
+ del self.cleanded_data['user_status']
+ raise forms.ValidationError(
+ _('Cannot turn other user to moderator')
+ )
+
+ #do not allow moderator to change status of other moderators
+ if self.moderator.is_moderator() and self.subject.is_moderator():
+ del self.cleaned_data['user_status']
+ raise forms.ValidationError(
+ _('Cannot change status of another moderator')
+ )
+
+ #do not allow moderator to change to admin
+ if self.moderator.is_moderator() and user_status == 'd':
+ raise forms.ValidationError(
+ _("Cannot change status to admin")
+ )
+
+ if user_status == 'select':
+ del self.cleaned_data['user_status']
+ msg = _(
+ 'If you wish to change %(username)s\'s status, '
+ 'please make a meaningful selection.'
+ ) % {'username': self.subject.username}
+ raise forms.ValidationError(msg)
+
+ return self.cleaned_data
+
+
+class SendMessageForm(forms.Form):
+ subject_line = forms.CharField(
+ label=_('Subject line'),
+ max_length=64,
+ widget=forms.TextInput(attrs={'size': 64}, )
+ )
+ body_text = forms.CharField(
+ label=_('Message text'),
+ max_length=1600,
+ widget=forms.Textarea(attrs={'cols': 64})
+ )
+
+
+class NotARobotForm(forms.Form):
+ recaptcha = RecaptchaField(
+ private_key=askbot_settings.RECAPTCHA_SECRET,
+ public_key=askbot_settings.RECAPTCHA_KEY
+ )
+
+
+class FeedbackForm(forms.Form):
+ name = forms.CharField(label=_('Your name (optional):'), required=False)
+ email = forms.EmailField(label=_('Email:'), required=False)
+ message = forms.CharField(
+ label=_('Your message:'),
+ max_length=800,
+ widget=forms.Textarea(attrs={'cols': 60})
+ )
+ no_email = forms.BooleanField(
+ label=_("I don't want to give my email or receive a response:"),
+ required=False
+ )
+ next = NextUrlField()
+
+ def __init__(self, is_auth=False, *args, **kwargs):
+ super(FeedbackForm, self).__init__(*args, **kwargs)
+ self.is_auth = is_auth
+ if not is_auth:
+ if askbot_settings.USE_RECAPTCHA:
+ self._add_recaptcha_field()
+
+ def _add_recaptcha_field(self):
+ self.fields['recaptcha'] = RecaptchaField(
+ private_key=askbot_settings.RECAPTCHA_SECRET,
+ public_key=askbot_settings.RECAPTCHA_KEY
+ )
+
+ def clean(self):
+ super(FeedbackForm, self).clean()
+ if not self.is_auth:
+ if not self.cleaned_data['no_email'] \
+ and not self.cleaned_data['email']:
+ msg = _('Please mark "I dont want to give my mail" field.')
+ self._errors['email'] = self.error_class([msg])
+
+ return self.cleaned_data
+
+
+class FormWithHideableFields(object):
+ """allows to swap a field widget to HiddenInput() and back"""
+
+ def hide_field(self, name):
+ """replace widget with HiddenInput()
+ and save the original in the __hidden_fields dictionary
+ """
+ if not hasattr(self, '__hidden_fields'):
+ self.__hidden_fields = dict()
+ if name in self.__hidden_fields:
+ return
+ self.__hidden_fields[name] = self.fields[name].widget
+ self.fields[name].widget = forms.HiddenInput()
+
+ def show_field(self, name):
+ """restore the original widget on the field
+ if it was previously hidden
+ """
+ if name in self.__hidden_fields:
+ self.fields[name] = self.__hidden_fields.pop(name)
+
+
+class PostPrivatelyForm(forms.Form, FormWithHideableFields):
+ """has a single field `post_privately` with
+ two related methods"""
+
+ post_privately = forms.BooleanField(
+ label = _('keep private within your groups'),
+ required = False
+ )
+ def __init__(self, *args, **kwargs):
+ user = kwargs.pop('user', None)
+ self._user = user
+ super(PostPrivatelyForm, self).__init__(*args, **kwargs)
+ if self.allows_post_privately() == False:
+ self.hide_field('post_privately')
+
+ def allows_post_privately(self):
+ user = self._user
+ return (
+ askbot_settings.GROUPS_ENABLED and \
+ user and user.is_authenticated() and \
+ user.can_make_group_private_posts()
+ )
+
+ def clean_post_privately(self):
+ if self.allows_post_privately() == False:
+ self.cleaned_data['post_privately'] = False
+ return self.cleaned_data['post_privately']
+
+
+class DraftQuestionForm(forms.Form):
+ """No real validation required for this form"""
+ title = forms.CharField(required=False)
+ text = forms.CharField(required=False)
+ tagnames = forms.CharField(required=False)
+
+
+class DraftAnswerForm(forms.Form):
+ """Only thread_id is required"""
+ thread_id = forms.IntegerField()
+ text = forms.CharField(required=False)
+
+
+class PostAsSomeoneForm(forms.Form):
+ post_author_username = forms.CharField(
+ initial=_('User name:'),
+ help_text=_(
+ 'Enter name to post on behalf of someone else. '
+ 'Can create new accounts.'
+ ),
+ required=False,
+ widget=forms.TextInput()
+ )
+ post_author_email = forms.CharField(
+ initial=_('Email address:'),
+ required=False,
+ widget=forms.TextInput(attrs={'class': 'tipped-input'})
+ )
+
+ def get_post_user(self, user):
+ """returns user on whose behalf the post or a revision
+ is being made
+ """
+ username = self.cleaned_data['post_author_username']
+ email= self.cleaned_data['post_author_email']
+ if user.is_administrator() and username and email:
+ post_user = user.get_or_create_fake_user(username, email)
+ else:
+ post_user = user
+ return post_user
+
+ def clean_post_author_username(self):
+ """if value is the same as initial, it is reset to
+ empty string
+ todo: maybe better to have field where initial value is invalid,
+ then we would not have to have two almost identical clean functions?
+ """
+ username = self.cleaned_data.get('post_author_username', '')
+ initial_username = unicode(self.fields['post_author_username'].initial)
+ if username == initial_username:
+ self.cleaned_data['post_author_username'] = ''
+ return self.cleaned_data['post_author_username']
+
+ def clean_post_author_email(self):
+ """if value is the same as initial, it is reset to
+ empty string"""
+ email = self.cleaned_data.get('post_author_email', '')
+ initial_email = unicode(self.fields['post_author_email'].initial)
+ if email == initial_email:
+ email = ''
+ if email != '':
+ email = forms.EmailField().clean(email)
+ self.cleaned_data['post_author_email'] = email
+ return email
+
+ def clean(self):
+ """requires email address if user name is given"""
+ username = self.cleaned_data.get('post_author_username', '')
+ email = self.cleaned_data.get('post_author_email', '')
+ if username == '' and email:
+ username_errors = self._errors.get(
+ 'post_author_username',
+ ErrorList()
+ )
+ username_errors.append(_('User name is required with the email'))
+ self._errors['post_author_username'] = username_errors
+ raise forms.ValidationError('missing user name')
+ elif email == '' and username:
+ email_errors = self._errors.get('post_author_email', ErrorList())
+ email_errors.append(_('Email is required if user name is added'))
+ self._errors['post_author_email'] = email_errors
+ raise forms.ValidationError('missing email')
+
+ return self.cleaned_data
+
+
+class AskForm(PostAsSomeoneForm, PostPrivatelyForm):
+ """the form used to askbot questions
+ field ask_anonymously is shown to the user if the
+ if ALLOW_ASK_ANONYMOUSLY live setting is True
+ however, for simplicity, the value will always be present
+ in the cleaned data, and will evaluate to False if the
+ settings forbids anonymous asking
+ """
+ title = TitleField()
+ tags = TagNamesField()
+ wiki = WikiField()
+ group_id = forms.IntegerField(required = False, widget = forms.HiddenInput)
+ ask_anonymously = forms.BooleanField(
+ label=_('ask anonymously'),
+ help_text=_(
+ 'Check if you do not want to reveal your name '
+ 'when asking this question'
+ ),
+ required=False,
+ )
+ openid = forms.CharField(
+ required=False, max_length=255,
+ widget=forms.TextInput(attrs={'size': 40, 'class': 'openid-input'})
+ )
+
+ def __init__(self, *args, **kwargs):
+ super(AskForm, self).__init__(*args, **kwargs)
+ #it's important that this field is set up dynamically
+ self.fields['text'] = QuestionEditorField()
+ #hide ask_anonymously field
+ if askbot_settings.ALLOW_ASK_ANONYMOUSLY is False:
+ self.hide_field('ask_anonymously')
+
+ def clean_ask_anonymously(self):
+ """returns false if anonymous asking is not allowed
+ """
+ if askbot_settings.ALLOW_ASK_ANONYMOUSLY is False:
+ self.cleaned_data['ask_anonymously'] = False
+ return self.cleaned_data['ask_anonymously']
+
+
+ASK_BY_EMAIL_SUBJECT_HELP = _(
+ 'Subject line is expected in the format: '
+ '[tag1, tag2, tag3,...] question title'
+)
+
+#widgetforms
+class AskWidgetForm(forms.Form, FormWithHideableFields):
+ '''Simple form with just the title to ask a question'''
+
+ title = TitleField()
+ ask_anonymously = forms.BooleanField(
+ label=_('ask anonymously'),
+ help_text=_(
+ 'Check if you do not want to reveal your name '
+ 'when asking this question'
+ ),
+ required=False,
+ )
+
+ def __init__(self, include_text=True, *args, **kwargs):
+ super(AskWidgetForm, self).__init__(*args, **kwargs)
+ #hide ask_anonymously field
+ if not askbot_settings.ALLOW_ASK_ANONYMOUSLY:
+ self.hide_field('ask_anonymously')
+ self.fields['text'] = QuestionEditorField()
+ if not include_text:
+ self.hide_field('text')
+ #hack to make it validate
+ self.fields['text'].required = False
+ self.fields['text'].min_length = 0
+
+class CreateAskWidgetForm(forms.Form, FormWithHideableFields):
+ title = forms.CharField(max_length=100)
+ include_text_field = forms.BooleanField(required=False)
+
+ inner_style = forms.CharField(
+ widget=forms.Textarea,
+ required=False
+ )
+ outer_style = forms.CharField(
+ widget=forms.Textarea,
+ required=False
+ )
+
+ def __init__(self, *args, **kwargs):
+ from askbot.models import Group, Tag
+ super(CreateAskWidgetForm, self).__init__(*args, **kwargs)
+ self.fields['group'] = forms.ModelChoiceField(
+ queryset=Group.objects.exclude_personal(),
+ required=False
+ )
+ self.fields['tag'] = forms.ModelChoiceField(queryset=Tag.objects.get_content_tags(),
+ required=False)
+ if not askbot_settings.GROUPS_ENABLED:
+ self.hide_field('group')
+
+class CreateQuestionWidgetForm(forms.Form, FormWithHideableFields):
+ title = forms.CharField(max_length=100)
+ question_number = forms.CharField(initial='7')
+ tagnames = forms.CharField(label=_('tags'), max_length=50)
+ search_query = forms.CharField(max_length=50, required=False)
+ order_by = forms.ChoiceField(
+ choices=const.SEARCH_ORDER_BY,
+ initial='-added_at'
+ )
+ style = forms.CharField(
+ widget=forms.Textarea,
+ initial=const.DEFAULT_QUESTION_WIDGET_STYLE,
+ required=False
+ )
+
+ def __init__(self, *args, **kwargs):
+ from askbot.models import Group
+ super(CreateQuestionWidgetForm, self).__init__(*args, **kwargs)
+ self.fields['tagnames'] = TagNamesField()
+ self.fields['group'] = forms.ModelChoiceField(
+ queryset=Group.objects.exclude(name__startswith='_internal'),
+ required=False
+ )
+
+class AskByEmailForm(forms.Form):
+ """:class:`~askbot.forms.AskByEmailForm`
+ validates question data, where question was posted
+ by email.
+
+ It is ivoked by the management command
+ :mod:`~askbot.management.commands.post_emailed_questions`
+
+ Input is text data with attributes:
+
+ * :attr:`~askbot.forms.AskByEmailForm.sender` - unparsed "from" data
+ * :attr:`~askbot.forms.AskByEmailForm.subject` - subject line
+ * :attr:`~askbot.forms.AskByEmailForm.body_text` - body text of the email
+
+ Cleaned values are:
+ * ``email`` - email address
+ * ``title`` - question title
+ * ``tagnames`` - tag names all in one string
+ * ``body_text`` - body of question text -
+ a pass-through, no extra validation
+ """
+ sender = forms.CharField(max_length=255)
+ subject = forms.CharField(
+ max_length=255,
+ error_messages={
+ 'required': ASK_BY_EMAIL_SUBJECT_HELP
+ }
+ )
+ body_text = QuestionEditorField()
+
+ def clean_sender(self):
+ """Cleans the :attr:`~askbot.forms.AskByEmail.sender` attribute
+
+ If the field is valid, cleaned data will receive value ``email``
+ """
+ raw_email = self.cleaned_data['sender']
+ email = extract_first_email_address(raw_email)
+ if email is None:
+ raise forms.ValidationError('Could not extract email address')
+ self.cleaned_data['email'] = email
+ return self.cleaned_data['sender']
+
+ def clean_subject(self):
+ """Cleans the :attr:`~askbot.forms.AskByEmail.subject` attribute
+
+ If the field is valid, cleaned data will receive values
+ ``tagnames`` and ``title``
+ """
+ raw_subject = self.cleaned_data['subject'].strip()
+ if askbot_settings.TAGS_ARE_REQUIRED:
+ subject_re = re.compile(r'^\[([^]]+)\](.*)$')
+ else:
+ subject_re = re.compile(r'^(?:\[([^]]+)\])?(.*)$')
+ match = subject_re.match(raw_subject)
+ if match:
+ #make raw tags comma-separated
+ if match.group(1) is None: # no tags
+ self.cleaned_data['tagnames'] = ''
+ else:
+ tagnames = match.group(1).replace(';', ',')
+
+ #pre-process tags
+ tag_list = [tag.strip() for tag in tagnames.split(',')]
+ tag_list = [re.sub(r'\s+', ' ', tag) for tag in tag_list]
+
+ if askbot_settings.REPLACE_SPACE_WITH_DASH_IN_EMAILED_TAGS:
+ tag_list = [tag.replace(' ', '-') for tag in tag_list]
+ #todo: use tag separator char here
+ tagnames = ' '.join(tag_list)
+
+ #clean tags - may raise ValidationError
+ self.cleaned_data['tagnames'] = TagNamesField().clean(tagnames)
+
+ #clean title - may raise ValidationError
+ title = match.group(2).strip()
+ self.cleaned_data['title'] = TitleField().clean(title)
+ else:
+ raise forms.ValidationError(ASK_BY_EMAIL_SUBJECT_HELP)
+ return self.cleaned_data['subject']
+
+
+class AnswerForm(PostAsSomeoneForm, PostPrivatelyForm):
+ text = AnswerEditorField()
+ wiki = WikiField()
+ openid = forms.CharField(
+ required=False, max_length=255,
+ widget=forms.TextInput(attrs={'size': 40, 'class': 'openid-input'})
+ )
+ email_notify = EmailNotifyField(initial=False)
+
+ def __init__(self, *args, **kwargs):
+ super(AnswerForm, self).__init__(*args, **kwargs)
+ self.fields['text'] = AnswerEditorField()
+ self.fields['email_notify'].widget.attrs['id'] = \
+ 'question-subscribe-updates'
+
+
+class VoteForm(forms.Form):
+ """form used in ajax vote view (only comment_upvote so far)
+ """
+ post_id = forms.IntegerField()
+ # char because it is 'true' or 'false' as string
+ cancel_vote = forms.CharField()
+
+ def clean_cancel_vote(self):
+ val = self.cleaned_data['cancel_vote']
+ if val == 'true':
+ result = True
+ elif val == 'false':
+ result = False
+ else:
+ del self.cleaned_data['cancel_vote']
+ raise forms.ValidationError(
+ 'either "true" or "false" strings expected'
+ )
+ self.cleaned_data['cancel_vote'] = result
+ return self.cleaned_data['cancel_vote']
+
+
+class CloseForm(forms.Form):
+ reason = forms.ChoiceField(choices=const.CLOSE_REASONS)
+
+
+class RetagQuestionForm(forms.Form):
+ tags = TagNamesField()
+
+ def __init__(self, question, *args, **kwargs):
+ """initialize the default values"""
+ super(RetagQuestionForm, self).__init__(*args, **kwargs)
+ self.fields['tags'].initial = question.thread.tagnames
+
+
+class RevisionForm(forms.Form):
+ """
+ Lists revisions of a Question or Answer
+ """
+ revision = forms.ChoiceField(
+ widget=forms.Select(
+ attrs={'style': 'width:520px'}
+ )
+ )
+
+ def __init__(self, post, latest_revision, *args, **kwargs):
+ super(RevisionForm, self).__init__(*args, **kwargs)
+ revisions = post.revisions.values_list(
+ 'revision', 'author__username', 'revised_at', 'summary'
+ )
+ date_format = '%c'
+ rev_choices = list()
+ for r in revisions:
+ rev_details = u'%s - %s (%s) %s' % (
+ r[0], r[1], r[2].strftime(date_format), r[3]
+ )
+ rev_choices.append((r[0], rev_details))
+
+ self.fields['revision'].choices = rev_choices
+ self.fields['revision'].initial = latest_revision.revision
+
+class EditQuestionForm(PostAsSomeoneForm, PostPrivatelyForm):
+ title = TitleField()
+ tags = TagNamesField()
+ summary = SummaryField()
+ wiki = WikiField()
+ reveal_identity = forms.BooleanField(
+ help_text=_(
+ 'You have asked this question anonymously, '
+ 'if you decide to reveal your identity, please check '
+ 'this box.'
+ ),
+ label=_('reveal identity'),
+ required=False,
+ )
+
+ #todo: this is odd that this form takes question as an argument
+ def __init__(self, *args, **kwargs):
+ """populate EditQuestionForm with initial data"""
+ self.question = kwargs.pop('question')
+ self.user = kwargs['user']#preserve for superclass
+ revision = kwargs.pop('revision')
+ super(EditQuestionForm, self).__init__(*args, **kwargs)
+ #it is important to add this field dynamically
+ self.fields['text'] = QuestionEditorField()
+ self.fields['title'].initial = revision.title
+ self.fields['text'].initial = revision.text
+ self.fields['tags'].initial = revision.tagnames
+ self.fields['wiki'].initial = self.question.wiki
+ #hide the reveal identity field
+ if not self.can_stay_anonymous():
+ self.hide_field('reveal_identity')
+
+ def has_changed(self):
+ if super(EditQuestionForm, self).has_changed():
+ return True
+ if askbot_settings.GROUPS_ENABLED:
+ return self.question.is_private() \
+ != self.cleaned_data['post_privately']
+ else:
+ return False
+
+ def can_stay_anonymous(self):
+ """determines if the user cat keep editing the question
+ anonymously"""
+ return (askbot_settings.ALLOW_ASK_ANONYMOUSLY
+ and self.question.is_anonymous
+ and self.user.is_owner_of(self.question)
+ )
+
+ def clean_reveal_identity(self):
+ """cleans the reveal_identity field
+ which determines whether previous anonymous
+ edits must be rewritten as not anonymous
+ this does not necessarily mean that the edit will be anonymous
+
+ only does real work when question is anonymous
+ based on the following truth table:
+
+ is_anon can owner checked cleaned data
+ - * * * False (ignore choice in checkbox)
+ + + + + True
+ + + + - False
+ + + - + Raise(Not owner)
+ + + - - False
+ + - + + True (setting "can" changed, say yes)
+ + - + - False, warn (but prev edits stay anon)
+ + - - + Raise(Not owner)
+ + - - - False
+ """
+ value = self.cleaned_data['reveal_identity']
+ if self.question.is_anonymous:
+ if value is True:
+ if self.user.is_owner_of(self.question):
+ #regardless of the ALLOW_ASK_ANONYMOUSLY
+ return True
+ else:
+ self.show_field('reveal_identity')
+ del self.cleaned_data['reveal_identity']
+ raise forms.ValidationError(
+ _(
+ 'Sorry, only owner of the anonymous '
+ 'question can reveal his or her '
+ 'identity, please uncheck the '
+ 'box'
+ )
+ )
+ else:
+ can_ask_anon = askbot_settings.ALLOW_ASK_ANONYMOUSLY
+ is_owner = self.user.is_owner_of(self.question)
+ if can_ask_anon is False and is_owner:
+ self.show_field('reveal_identity')
+ raise forms.ValidationError(
+ _(
+ 'Sorry, apparently rules have just changed - '
+ 'it is no longer possible to ask anonymously. '
+ 'Please either check the "reveal identity" box '
+ 'or reload this page and try editing the question '
+ 'again.'
+ )
+ )
+ return False
+ else:
+ #takes care of 8 possibilities - first row of the table
+ return False
+
+ def clean(self):
+ """Purpose of this function is to determine whether
+ it is ok to apply edit anonymously in the synthetic
+ field edit_anonymously. It relies on correct cleaning
+ if the "reveal_identity" field
+ """
+ super(EditQuestionForm, self).clean()
+ reveal_identity = self.cleaned_data.get('reveal_identity', False)
+ stay_anonymous = False
+ if reveal_identity is False and self.can_stay_anonymous():
+ stay_anonymous = True
+ self.cleaned_data['stay_anonymous'] = stay_anonymous
+ return self.cleaned_data
+
+class EditAnswerForm(PostAsSomeoneForm, PostPrivatelyForm):
+ summary = SummaryField()
+ wiki = WikiField()
+
+ def __init__(self, answer, revision, *args, **kwargs):
+ self.answer = answer
+ super(EditAnswerForm, self).__init__(*args, **kwargs)
+ #it is important to add this field dynamically
+ self.fields['text'] = AnswerEditorField()
+ self.fields['text'].initial = revision.text
+ self.fields['wiki'].initial = answer.wiki
+
+ def has_changed(self):
+ #todo: this function is almost copy/paste of EditQuestionForm.has_changed()
+ if super(EditAnswerForm, self).has_changed():
+ return True
+ if askbot_settings.GROUPS_ENABLED:
+ return self.answer.is_private() \
+ != self.cleaned_data['post_privately']
+ else:
+ return False
+
+
+class EditTagWikiForm(forms.Form):
+ text = forms.CharField(required=False)
+ tag_id = forms.IntegerField()
+
+
+class EditUserForm(forms.Form):
+ email = forms.EmailField(
+ label=u'Email',
+ required=True,
+ max_length=255,
+ widget=forms.TextInput(attrs={'size': 35})
+ )
+
+ realname = forms.CharField(
+ label=_('Real name'),
+ required=False,
+ max_length=255,
+ widget=forms.TextInput(attrs={'size': 35})
+ )
+
+ website = forms.URLField(
+ label=_('Website'),
+ required=False,
+ max_length=255,
+ widget=forms.TextInput(attrs={'size': 35})
+ )
+
+ city = forms.CharField(
+ label=_('City'),
+ required=False,
+ max_length=255,
+ widget=forms.TextInput(attrs={'size': 35})
+ )
+
+ country = CountryField(required=False)
+
+ show_country = forms.BooleanField(
+ label=_('Show country'),
+ required=False
+ )
+
+ show_marked_tags = forms.BooleanField(
+ label=_('Show tag choices'),
+ required=False
+ )
+
+ birthday = forms.DateField(
+ label=_('Date of birth'),
+ help_text=_(
+ 'will not be shown, used to calculate '
+ 'age, format: YYYY-MM-DD'
+ ),
+ required=False,
+ widget=forms.TextInput(attrs={'size': 35})
+ )
+
+ about = forms.CharField(
+ label=_('Profile'),
+ required=False,
+ widget=forms.Textarea(attrs={'cols': 60})
+ )
+
+ def __init__(self, user, *args, **kwargs):
+ super(EditUserForm, self).__init__(*args, **kwargs)
+ logging.debug('initializing the form')
+ if askbot_settings.EDITABLE_SCREEN_NAME:
+ self.fields['username'] = UserNameField(label=_('Screen name'))
+ self.fields['username'].initial = user.username
+ self.fields['username'].user_instance = user
+ self.fields['email'].initial = user.email
+ self.fields['realname'].initial = user.real_name
+ self.fields['website'].initial = user.website
+ self.fields['city'].initial = user.location
+ if user.country is None:
+ country = 'unknown'
+ else:
+ country = user.country
+ self.fields['country'].initial = country
+ self.fields['show_country'].initial = user.show_country
+ self.fields['show_marked_tags'].initial = user.show_marked_tags
+
+ if user.date_of_birth is not None:
+ self.fields['birthday'].initial = user.date_of_birth
+
+ self.fields['about'].initial = user.about
+ self.user = user
+
+ def clean_email(self):
+ """For security reason one unique email in database"""
+ if self.user.email != self.cleaned_data['email']:
+ #todo dry it, there is a similar thing in openidauth
+ if askbot_settings.EMAIL_UNIQUE is True:
+ if 'email' in self.cleaned_data:
+ try:
+ User.objects.get(email=self.cleaned_data['email'])
+ except User.DoesNotExist:
+ return self.cleaned_data['email']
+ except User.MultipleObjectsReturned:
+ raise forms.ValidationError(_(
+ 'this email has already been registered, '
+ 'please use another one')
+ )
+ raise forms.ValidationError(_(
+ 'this email has already been registered, '
+ 'please use another one')
+ )
+ return self.cleaned_data['email']
+
+
+class TagFilterSelectionForm(forms.ModelForm):
+ email_tag_filter_strategy = forms.ChoiceField(
+ initial = const.EXCLUDE_IGNORED,
+ label = _('Choose email tag filter'),
+ widget = forms.RadioSelect
+ )
+ def __init__(self, *args, **kwargs):
+ super(TagFilterSelectionForm, self).__init__(*args, **kwargs)
+ choices = get_tag_display_filter_strategy_choices()
+ self.fields['email_tag_filter_strategy'].choices = choices
+
+ class Meta:
+ model = User
+ fields = ('email_tag_filter_strategy',)
+
+ def save(self):
+ before = self.instance.email_tag_filter_strategy
+ super(TagFilterSelectionForm, self).save()
+ after = self.instance.email_tag_filter_strategy
+ if before != after:
+ return True
+ return False
+
+
+class EmailFeedSettingField(forms.ChoiceField):
+ def __init__(self, *arg, **kwarg):
+ kwarg['choices'] = const.NOTIFICATION_DELIVERY_SCHEDULE_CHOICES
+ kwarg['widget'] = forms.RadioSelect
+ super(EmailFeedSettingField, self).__init__(*arg, **kwarg)
+
+
+class EditUserEmailFeedsForm(forms.Form):
+ FORM_TO_MODEL_MAP = {
+ 'all_questions': 'q_all',
+ 'asked_by_me': 'q_ask',
+ 'answered_by_me': 'q_ans',
+ 'individually_selected': 'q_sel',
+ 'mentions_and_comments': 'm_and_c',
+ }
+ NO_EMAIL_INITIAL = {
+ 'all_questions': 'n',
+ 'asked_by_me': 'n',
+ 'answered_by_me': 'n',
+ 'individually_selected': 'n',
+ 'mentions_and_comments': 'n',
+ }
+ INSTANT_EMAIL_INITIAL = {
+ 'all_questions': 'i',
+ 'asked_by_me': 'i',
+ 'answered_by_me': 'i',
+ 'individually_selected': 'i',
+ 'mentions_and_comments': 'i',
+ }
+
+ asked_by_me = EmailFeedSettingField(
+ label=_('Asked by me')
+ )
+ answered_by_me = EmailFeedSettingField(
+ label=_('Answered by me')
+ )
+ individually_selected = EmailFeedSettingField(
+ label=_('Individually selected')
+ )
+ all_questions = EmailFeedSettingField(
+ label=_('Entire forum (tag filtered)'),
+ )
+
+ mentions_and_comments = EmailFeedSettingField(
+ label=_('Comments and posts mentioning me'),
+ )
+
+ def set_initial_values(self, user=None):
+ from askbot import models
+ KEY_MAP = dict([(v, k) for k, v in self.FORM_TO_MODEL_MAP.iteritems()])
+ if user is not None:
+ settings = models.EmailFeedSetting.objects.filter(subscriber=user)
+ initial_values = {}
+ for setting in settings:
+ feed_type = setting.feed_type
+ form_field = KEY_MAP[feed_type]
+ frequency = setting.frequency
+ initial_values[form_field] = frequency
+ self.initial = initial_values
+ return self
+
+ def reset(self):
+ """equivalent to set_frequency('n')
+ but also returns self due to some legacy requirement
+ todo: clean up use of this function
+ """
+ if self.is_bound:
+ self.cleaned_data = self.NO_EMAIL_INITIAL
+ self.initial = self.NO_EMAIL_INITIAL
+ return self
+
+ def get_db_model_subscription_type_names(self):
+ """todo: refactor this - too hacky
+ should probably use model form instead
+
+ returns list of values acceptable in
+ ``attr::models.user.EmailFeedSetting.feed_type``
+ """
+ return self.FORM_TO_MODEL_MAP.values()
+
+ def set_frequency(self, frequency='n'):
+ data = {
+ 'all_questions': frequency,
+ 'asked_by_me': frequency,
+ 'answered_by_me': frequency,
+ 'individually_selected': frequency,
+ 'mentions_and_comments': frequency
+ }
+ if self.is_bound:
+ self.cleaned_data = data
+ self.initial = data
+
+ def save(self, user, save_unbound=False):
+ """with save_unbound==True will bypass form
+ validation and save initial values
+ """
+ from askbot import models
+ changed = False
+ for form_field, feed_type in self.FORM_TO_MODEL_MAP.items():
+ s, created = models.EmailFeedSetting.objects.get_or_create(
+ subscriber=user,
+ feed_type=feed_type
+ )
+ if save_unbound:
+ #just save initial values instead
+ if form_field in self.initial:
+ new_value = self.initial[form_field]
+ else:
+ new_value = self.fields[form_field].initial
+ else:
+ new_value = self.cleaned_data[form_field]
+ if s.frequency != new_value:
+ s.frequency = new_value
+ s.save()
+ changed = True
+ else:
+ if created:
+ s.save()
+ if form_field == 'individually_selected':
+ user.followed_threads.clear()
+ return changed
+
+
+class SubscribeForEmailUpdatesField(forms.ChoiceField):
+ """a simple yes or no field to subscribe for email or not"""
+ def __init__(self, **kwargs):
+ kwargs['widget'] = forms.widgets.RadioSelect
+ kwargs['error_messages'] = {
+ 'required': _('please choose one of the options above')
+ }
+ kwargs['choices'] = (
+ ('y', _('okay, let\'s try!')),
+ (
+ 'n',
+ _('no %(sitename)s email please, thanks')
+ % {'sitename': askbot_settings.APP_SHORT_NAME}
+ )
+ )
+ super(SubscribeForEmailUpdatesField, self).__init__(**kwargs)
+
+
+class SimpleEmailSubscribeForm(forms.Form):
+ subscribe = SubscribeForEmailUpdatesField()
+
+ def save(self, user=None):
+ EFF = EditUserEmailFeedsForm
+ #here we have kind of an anomaly - the value 'y' is redundant
+ #with the frequency variable - needs to be fixed
+ if self.is_bound and self.cleaned_data['subscribe'] == 'y':
+ email_settings_form = EFF()
+ email_settings_form.set_initial_values(user)
+ logging.debug('%s wants to subscribe' % user.username)
+ else:
+ email_settings_form = EFF(initial=EFF.NO_EMAIL_INITIAL)
+ email_settings_form.save(user, save_unbound=True)
+
+
+class GroupLogoURLForm(forms.Form):
+ """form for saving group logo url"""
+ group_id = forms.IntegerField()
+ image_url = forms.CharField()
+
+
+class EditGroupMembershipForm(forms.Form):
+ """a form for adding or removing users
+ to and from user groups"""
+ user_id = forms.IntegerField()
+ group_name = forms.CharField()
+ action = forms.CharField()
+
+ def clean_action(self):
+ """allowed actions are 'add' and 'remove'"""
+ action = self.cleaned_data['action']
+ if action not in ('add', 'remove'):
+ del self.cleaned_data['action']
+ raise forms.ValidationError('invalid action')
+ return action
+
+
+class EditRejectReasonForm(forms.Form):
+ reason_id = forms.IntegerField(required=False)
+ title = CountedWordsField(
+ min_words=1, max_words=4, field_name=_('Title')
+ )
+ details = CountedWordsField(
+ min_words=6, field_name=_('Description')
+ )
+
+class ModerateTagForm(forms.Form):
+ tag_id = forms.IntegerField()
+ thread_id = forms.IntegerField(required = False)
+ action = forms.CharField()
+
+ def clean_action(self):
+ action = self.cleaned_data['action']
+ assert(action in ('accept', 'reject'))
+ return action
+
+class ShareQuestionForm(forms.Form):
+ thread_id = forms.IntegerField()
+ recipient_name = forms.CharField()
diff --git a/askbot/mail/__init__.py b/askbot/mail/__init__.py
index 7d426e80..c9c84f33 100644
--- a/askbot/mail/__init__.py
+++ b/askbot/mail/__init__.py
@@ -366,10 +366,10 @@ def process_emailed_question(
'subject': subject,
'body_text': body_text
}
- form = AskByEmailForm(data)
+ user = User.objects.get(email__iexact = email_address)
+ form = AskByEmailForm(data, user=user)
if form.is_valid():
email_address = form.cleaned_data['email']
- user = User.objects.get(email__iexact = email_address)
if user.can_post_by_email() is False:
raise PermissionDenied(messages.insufficient_reputation(user))
diff --git a/askbot/mail/__init__.py.orig b/askbot/mail/__init__.py.orig
new file mode 100644
index 00000000..7d426e80
--- /dev/null
+++ b/askbot/mail/__init__.py.orig
@@ -0,0 +1,431 @@
+"""functions that send email in askbot
+these automatically catch email-related exceptions
+"""
+import os
+import smtplib
+import logging
+from django.core import mail
+from django.conf import settings as django_settings
+from django.core.exceptions import PermissionDenied
+from django.forms import ValidationError
+from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import string_concat
+from django.template import Context
+from django.utils.html import strip_tags
+from askbot import exceptions
+from askbot import const
+from askbot.conf import settings as askbot_settings
+from askbot.utils import url_utils
+from askbot.utils.file_utils import store_file
+from askbot.utils.html import absolutize_urls
+
+from bs4 import BeautifulSoup
+#todo: maybe send_mail functions belong to models
+#or the future API
+def prefix_the_subject_line(subject):
+ """prefixes the subject line with the
+ EMAIL_SUBJECT_LINE_PREFIX either from
+ from live settings, which take default from django
+ """
+ prefix = askbot_settings.EMAIL_SUBJECT_PREFIX
+ if prefix != '':
+ subject = prefix.strip() + ' ' + subject.strip()
+ return subject
+
+def extract_first_email_address(text):
+ """extract first matching email address
+ from text string
+ returns ``None`` if there are no matches
+ """
+ match = const.EMAIL_REGEX.search(text)
+ if match:
+ return match.group(0)
+ else:
+ return None
+
+def thread_headers(post, orig_post, update):
+ """modify headers for email messages, so
+ that emails appear as threaded conversations in gmail"""
+ suffix_id = django_settings.SERVER_EMAIL
+ if update == const.TYPE_ACTIVITY_ASK_QUESTION:
+ msg_id = "NQ-%s-%s" % (post.id, suffix_id)
+ headers = {'Message-ID': msg_id}
+ elif update == const.TYPE_ACTIVITY_ANSWER:
+ msg_id = "NA-%s-%s" % (post.id, suffix_id)
+ orig_id = "NQ-%s-%s" % (orig_post.id, suffix_id)
+ headers = {'Message-ID': msg_id,
+ 'In-Reply-To': orig_id}
+ elif update == const.TYPE_ACTIVITY_UPDATE_QUESTION:
+ msg_id = "UQ-%s-%s-%s" % (post.id, post.last_edited_at, suffix_id)
+ orig_id = "NQ-%s-%s" % (orig_post.id, suffix_id)
+ headers = {'Message-ID': msg_id,
+ 'In-Reply-To': orig_id}
+ elif update == const.TYPE_ACTIVITY_COMMENT_QUESTION:
+ msg_id = "CQ-%s-%s" % (post.id, suffix_id)
+ orig_id = "NQ-%s-%s" % (orig_post.id, suffix_id)
+ headers = {'Message-ID': msg_id,
+ 'In-Reply-To': orig_id}
+ elif update == const.TYPE_ACTIVITY_UPDATE_ANSWER:
+ msg_id = "UA-%s-%s-%s" % (post.id, post.last_edited_at, suffix_id)
+ orig_id = "NQ-%s-%s" % (orig_post.id, suffix_id)
+ headers = {'Message-ID': msg_id,
+ 'In-Reply-To': orig_id}
+ elif update == const.TYPE_ACTIVITY_COMMENT_ANSWER:
+ msg_id = "CA-%s-%s" % (post.id, suffix_id)
+ orig_id = "NQ-%s-%s" % (orig_post.id, suffix_id)
+ headers = {'Message-ID': msg_id,
+ 'In-Reply-To': orig_id}
+ else:
+ # Unknown type -> Can't set headers
+ return {}
+
+ return headers
+
+def clean_html_email(email_body):
+ """needs more clenup might not work for other email templates
+ that do not use table layout
+ """
+ soup = BeautifulSoup(email_body)
+ body_element = soup.find('body')
+ phrases = map(
+ lambda s: s.strip(),
+ filter(bool, body_element.get_text().split('\n'))
+ )
+ return '\n\n'.join(phrases)
+
+def send_mail(
+ subject_line = None,
+ body_text = None,
+ from_email = django_settings.DEFAULT_FROM_EMAIL,
+ recipient_list = None,
+ activity_type = None,
+ related_object = None,
+ headers = None,
+ raise_on_failure = False,
+ ):
+ """
+ todo: remove parameters not relevant to the function
+ sends email message
+ logs email sending activity
+ and any errors are reported as critical
+ in the main log file
+
+ related_object is not mandatory, other arguments
+ are. related_object (if given, will be saved in
+ the activity record)
+
+ if raise_on_failure is True, exceptions.EmailNotSent is raised
+ """
+ body_text = absolutize_urls(body_text)
+ try:
+ assert(subject_line is not None)
+ subject_line = prefix_the_subject_line(subject_line)
+ msg = mail.EmailMultiAlternatives(
+ subject_line,
+ clean_html_email(body_text),
+ from_email,
+ recipient_list,
+ headers = headers
+ )
+ msg.attach_alternative(body_text, "text/html")
+ msg.send()
+ if related_object is not None:
+ assert(activity_type is not None)
+ except Exception, error:
+ logging.critical(unicode(error))
+ if raise_on_failure == True:
+ raise exceptions.EmailNotSent(unicode(error))
+
+def mail_moderators(
+ subject_line = '',
+ body_text = '',
+ raise_on_failure = False,
+ headers = None
+ ):
+ """sends email to forum moderators and admins
+ """
+ body_text = absolutize_urls(body_text)
+ from django.db.models import Q
+ from askbot.models import User
+ recipient_list = User.objects.filter(
+ Q(status='m') | Q(is_superuser=True)
+ ).filter(
+ is_active = True
+ ).values_list('email', flat=True)
+ recipient_list = set(recipient_list)
+
+ from_email = ''
+ if hasattr(django_settings, 'DEFAULT_FROM_EMAIL'):
+ from_email = django_settings.DEFAULT_FROM_EMAIL
+
+ try:
+ msg = mail.EmailMessage(
+ subject_line,
+ body_text,
+ from_email,
+ recipient_list,
+ headers = headers or {}
+ )
+ msg.content_subtype = 'html'
+ msg.send()
+ except smtplib.SMTPException, error:
+ logging.critical(unicode(error))
+ if raise_on_failure == True:
+ raise exceptions.EmailNotSent(unicode(error))
+
+INSTRUCTIONS_PREAMBLE = _('<p>To ask by email, please:</p>')
+QUESTION_TITLE_INSTRUCTION = _(
+ '<li>Type title in the subject line</li>'
+)
+QUESTION_DETAILS_INSTRUCTION = _(
+ '<li>Type details of your question into the email body</li>'
+)
+OPTIONAL_TAGS_INSTRUCTION = _(
+"""<li>The beginning of the subject line can contain tags,
+<em>enclosed in the square brackets</em> like so: [Tag1; Tag2]</li>"""
+)
+REQUIRED_TAGS_INSTRUCTION = _(
+"""<li>In the beginning of the subject add at least one tag
+<em>enclosed in the brackets</em> like so: [Tag1; Tag2].</li>"""
+)
+TAGS_INSTRUCTION_FOOTNOTE = _(
+"""<p>Note that a tag may consist of more than one word, to separate
+the tags, use a semicolon or a comma, for example, [One tag; Other tag]</p>"""
+)
+
+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``
+ * ``body_text`` in an optional parameter that allows to append
+ extra text to the message
+ """
+ if reason == 'problem_posting':
+ error_message = _(
+ '<p>Sorry, there was an error posting your question '
+ 'please contact the %(site)s administrator</p>'
+ ) % {'site': askbot_settings.APP_SHORT_NAME}
+
+ if askbot_settings.TAGS_ARE_REQUIRED:
+ error_message = string_concat(
+ INSTRUCTIONS_PREAMBLE,
+ '<ul>',
+ QUESTION_TITLE_INSTRUCTION,
+ REQUIRED_TAGS_INSTRUCTION,
+ QUESTION_DETAILS_INSTRUCTION,
+ '</ul>',
+ TAGS_INSTRUCTION_FOOTNOTE
+ )
+ else:
+ error_message = string_concat(
+ INSTRUCTIONS_PREAMBLE,
+ '<ul>',
+ QUESTION_TITLE_INSTRUCTION,
+ QUESTION_DETAILS_INSTRUCTION,
+ OPTIONAL_TAGS_INSTRUCTION,
+ '</ul>',
+ TAGS_INSTRUCTION_FOOTNOTE
+ )
+
+ elif reason == 'unknown_user':
+ error_message = _(
+ '<p>Sorry, in order to post questions on %(site)s '
+ 'by email, please <a href="%(url)s">register first</a></p>'
+ ) % {
+ 'site': askbot_settings.APP_SHORT_NAME,
+ 'url': url_utils.get_login_url()
+ }
+ 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)
+
+
+ #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,
+ headers = headers
+ )
+
+def extract_reply(text):
+ """take the part above the separator
+ and discard the last line above the separator
+ """
+ if const.REPLY_SEPARATOR_REGEX.search(text):
+ text = const.REPLY_SEPARATOR_REGEX.split(text)[0]
+ text_lines = text.splitlines(False)
+ #log last 10 lines of text - to capture email responses
+ logging.debug('reply-border-separator|' + '|'.join(text_lines[-10:]))
+ #here we need code stripping the "On ... somebody wrote:"
+ return '\n'.join(text_lines[:-3])
+ else:
+ return text
+
+def process_attachment(attachment):
+ """will save a single
+ attachment and return
+ link to file in the markdown format and the
+ file storage object
+ """
+ file_storage, file_name, file_url = store_file(attachment)
+ markdown_link = '[%s](%s) ' % (attachment.name, file_url)
+ file_extension = os.path.splitext(attachment.name)[1]
+ #todo: this is a hack - use content type
+ if file_extension.lower() in ('.png', '.jpg', '.jpeg', '.gif'):
+ markdown_link = '!' + markdown_link
+ return markdown_link, file_storage
+
+def extract_user_signature(text, reply_code):
+ """extracts email signature as text trailing
+ the reply code"""
+ striped_text = strip_tags(text)
+ if reply_code in striped_text:
+ #extract the signature
+ tail = list()
+ for line in reversed(striped_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.
+ Returns ready to post body of the message and the list
+ of uploaded files.
+ """
+ body_markdown = ''
+ stored_files = list()
+ attachments_markdown = ''
+ for (part_type, content) in parts:
+ if part_type == 'attachment':
+ markdown, stored_file = process_attachment(content)
+ stored_files.append(stored_file)
+ attachments_markdown += '\n\n' + markdown
+ elif part_type == 'body':
+ body_markdown += '\n\n' + content.strip('\n\t ')
+ elif part_type == 'inline':
+ markdown, stored_file = process_attachment(content)
+ stored_files.append(stored_file)
+ body_markdown += markdown
+
+ #if the response separator is present -
+ #split the body with it, and discard the "so and so wrote:" part
+ if reply_code:
+ signature = extract_user_signature(body_markdown, reply_code)
+ else:
+ signature = None
+ body_markdown = extract_reply(body_markdown)
+
+ body_markdown += attachments_markdown
+ return body_markdown.strip(), stored_files, signature
+
+
+def process_emailed_question(
+ from_address, subject, body_text, stored_files,
+ tags=None, group_id=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 ReplyAddress, User
+ from askbot.mail import messages
+
+ reply_to = None
+ try:
+ #todo: delete uploaded files when posting by email fails!!!
+ data = {
+ 'sender': from_address,
+ 'subject': subject,
+ 'body_text': body_text
+ }
+ form = AskByEmailForm(data)
+ if form.is_valid():
+ email_address = form.cleaned_data['email']
+ user = User.objects.get(email__iexact = email_address)
+
+ if user.can_post_by_email() is False:
+ raise PermissionDenied(messages.insufficient_reputation(user))
+
+ body_text = form.cleaned_data['body_text']
+ stripped_body_text = user.strip_email_signature(body_text)
+ signature_not_detected = (
+ stripped_body_text == body_text and user.email_signature
+ )
+
+ #ask for signature response if user's email has not been
+ #validated yet or if email signature could not be found
+ if user.email_isvalid is False or signature_not_detected:
+
+ 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']
+
+ #defect - here we might get "too many tags" issue
+ if tags:
+ tagnames += ' ' + ' '.join(tags)
+
+
+ user.post_question(
+ title=title,
+ tags=tagnames.strip(),
+ body_text=stripped_body_text,
+ by_email=True,
+ email_address=from_address,
+ group_id=group_id
+ )
+ else:
+ raise ValidationError()
+
+ except User.DoesNotExist:
+ bounce_email(email_address, subject, reason = 'unknown_user')
+ except User.MultipleObjectsReturned:
+ bounce_email(email_address, subject, reason = 'problem_posting')
+ except PermissionDenied, error:
+ bounce_email(
+ email_address,
+ subject,
+ reason = 'permission_denied',
+ body_text = unicode(error),
+ reply_to = reply_to
+ )
+ except ValidationError:
+ if from_address:
+ bounce_email(
+ from_address,
+ subject,
+ reason = 'problem_posting',
+ )
diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py
index ed687f86..4648748e 100644
--- a/askbot/models/__init__.py
+++ b/askbot/models/__init__.py
@@ -2490,8 +2490,8 @@ def user_fix_html_links(self, text):
result = replace_links_with_text(text)
if result != text:
message = ungettext(
- 'At least %d karma point is required to insert links',
- 'At least %d karma points is required to insert links',
+ 'At least %d karma point is required to post links',
+ 'At least %d karma points is required to post links',
askbot_settings.MIN_REP_TO_INSERT_LINK
) % askbot_settings.MIN_REP_TO_INSERT_LINK
self.message_set.create(message=message)
diff --git a/askbot/models/post.py b/askbot/models/post.py
index 4e841f13..da25f2fc 100644
--- a/askbot/models/post.py
+++ b/askbot/models/post.py
@@ -884,17 +884,6 @@ 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'
- ) % {
- 'user': parent_post.author.username,
- 'post': _(parent_post.post_type),
- 'date': parent_post.added_at.strftime(const.DATETIME_FORMAT)
- }
- output += '</p>'
- """
output += parent_post.format_for_email(
quote_level = quote_level,
format = 'parent_subthread'
diff --git a/askbot/models/question.py b/askbot/models/question.py
index 7faf589b..43844c82 100644
--- a/askbot/models/question.py
+++ b/askbot/models/question.py
@@ -702,7 +702,10 @@ class Thread(models.Model):
def format_for_email(self, user=None):
"""experimental function: output entire thread for email"""
- question, answers, junk, published_ans_ids = self.get_cached_post_data(user=user)
+
+ question, answers, junk, published_ans_ids = \
+ self.get_cached_post_data(user=user)
+
output = question.format_for_email_as_subthread()
if answers:
answer_heading = ungettext(
diff --git a/askbot/tests/form_tests.py b/askbot/tests/form_tests.py
index 90f4f4f2..fed38f87 100644
--- a/askbot/tests/form_tests.py
+++ b/askbot/tests/form_tests.py
@@ -1,5 +1,7 @@
from django import forms as django_forms
+from django.contrib.auth.models import AnonymousUser
from askbot.tests.utils import AskbotTestCase
+from askbot.tests.utils import with_settings
from askbot.conf import settings as askbot_settings
from askbot import forms
from askbot.utils import forms as util_forms
@@ -53,9 +55,10 @@ class AskByEmailFormTests(AskbotTestCase):
and makes sure that tags and title are parsed out"""
setting_backup = askbot_settings.TAGS_ARE_REQUIRED
askbot_settings.update('TAGS_ARE_REQUIRED', True)
+ user = AnonymousUser()
for test_case in SUBJECT_LINE_CASES:
self.data['subject'] = test_case[0]
- form = forms.AskByEmailForm(self.data)
+ form = forms.AskByEmailForm(self.data, user=user)
output = test_case[1]
if output is None:
self.assertFalse(form.is_valid())
@@ -75,10 +78,11 @@ class AskByEmailFormTests(AskbotTestCase):
"""loops through variants of the from field
in the emails and tests the email address
extractor"""
+ user = AnonymousUser()
for test_case in EMAIL_CASES:
self.data['sender'] = test_case[0]
expected_result = test_case[1]
- form = forms.AskByEmailForm(self.data)
+ form = forms.AskByEmailForm(self.data, user=user)
if expected_result is None:
self.assertFalse(form.is_valid())
else:
@@ -196,9 +200,9 @@ class EditQuestionAnonymouslyFormTests(AskbotTestCase):
data['reveal_identity'] = 'on'
self.form = forms.EditQuestionForm(
data,
- question = question,
- user = editor,
- revision = question.get_latest_revision(),
+ question=question,
+ user=editor,
+ revision=question.get_latest_revision(),
)
def test_reveal_identity_field(self):
@@ -230,7 +234,7 @@ class AskFormTests(AskbotTestCase):
}
if ask_anonymously == True:
data['ask_anonymously'] = 'on'
- self.form = forms.AskForm(data)
+ self.form = forms.AskForm(data, user=AnonymousUser())
self.form.full_clean()
def assert_anon_is(self, value):
@@ -317,7 +321,7 @@ class AnswerEditorFieldTests(AskbotTestCase):
def setUp(self):
self.old_min_length = askbot_settings.MIN_ANSWER_BODY_LENGTH
askbot_settings.update('MIN_ANSWER_BODY_LENGTH', 10)
- self.field = forms.AnswerEditorField()
+ self.field = forms.AnswerEditorField(user=AnonymousUser())
def tearDown(self):
askbot_settings.update('MIN_ANSWER_BODY_LENGTH', self.old_min_length)
@@ -364,8 +368,6 @@ class PostAsSomeoneFormTests(AskbotTestCase):
class AskWidgetFormTests(AskbotTestCase):
- form = forms.AskWidgetForm
-
def setUp(self):
self.good_data = {'title': "What's the price of a house in london?"}
self.good_data_anon = {'title': "What's the price of a house in london?",
@@ -374,12 +376,40 @@ class AskWidgetFormTests(AskbotTestCase):
self.bad_data = {'title': ''}
def test_valid_input(self):
- form_object = self.form(include_text=False, data=self.good_data)
- print form_object.errors
+ form_object = forms.AskWidgetForm(
+ include_text=False, data=self.good_data, user=AnonymousUser()
+ )
self.assertTrue(form_object.is_valid())
- form_object = self.form(include_text=False, data=self.good_data_anon)
+ form_object = forms.AskWidgetForm(
+ include_text=False, data=self.good_data_anon, user=AnonymousUser()
+ )
self.assertTrue(form_object.is_valid())
def test_invalid_input(self):
- form_object = self.form(False, data=self.bad_data)
+ form_object = forms.AskWidgetForm(
+ include_text=False, data=self.bad_data, user=AnonymousUser()
+ )
self.assertFalse(form_object.is_valid())
+
+
+TEXT_WITH_LINK = 'blah blah http://example.com/url/image.png'
+class EditorFieldTests(AskbotTestCase):
+
+ def setUp(self):
+ self.user = self.create_user('user')
+ self.user.reputation = 5
+ self.user.save()
+
+ @with_settings(EDITOR_TYPE='markdown', MIN_REP_TO_SUGGEST_LINK=10)
+ def test_low_rep_user_cannot_post_links_markdown(self):
+ field = forms.EditorField(user=self.user)
+ self.assertRaises(
+ django_forms.ValidationError, field.clean, TEXT_WITH_LINK
+ )
+
+ @with_settings(EDITOR_TYPE='tinymce', MIN_REP_TO_SUGGEST_LINK=10)
+ def test_low_rep_user_cannot_post_links_tinymce(self):
+ field = forms.EditorField(user=self.user)
+ self.assertRaises(
+ django_forms.ValidationError, field.clean, TEXT_WITH_LINK
+ )
diff --git a/askbot/views/commands.py b/askbot/views/commands.py
index 698bf2db..c072f7b8 100644
--- a/askbot/views/commands.py
+++ b/askbot/views/commands.py
@@ -1348,7 +1348,7 @@ def get_editor(request):
if 'config' not in request.GET:
return HttpResponseForbidden()
config = simplejson.loads(request.GET['config'])
- form = forms.EditorForm(editor_attrs=config)
+ form = forms.EditorForm(editor_attrs=config, user=request.user)
editor_html = render_text_into_skin(
'{{ form.media }} {{ form.editor }}',
{'form': form},
diff --git a/askbot/views/readers.py b/askbot/views/readers.py
index 5a3e378b..9801d5bc 100644
--- a/askbot/views/readers.py
+++ b/askbot/views/readers.py
@@ -549,7 +549,7 @@ def question(request, id):#refactor - long subroutine. display question body, an
if drafts.count() > 0:
initial['text'] = drafts[0].text
- answer_form = AnswerForm(initial)
+ answer_form = AnswerForm(initial, user=request.user)
user_can_post_comment = (
request.user.is_authenticated() and request.user.can_post_comment()
diff --git a/askbot/views/widgets.py b/askbot/views/widgets.py
index 8699cdf1..4401bbdc 100644
--- a/askbot/views/widgets.py
+++ b/askbot/views/widgets.py
@@ -61,8 +61,11 @@ def ask_widget(request, widget_id):
widget = get_object_or_404(models.AskWidget, id=widget_id)
if request.method == "POST":
- form = forms.AskWidgetForm(include_text=widget.include_text_field,
- data=request.POST)
+ form = forms.AskWidgetForm(
+ include_text=widget.include_text_field,
+ data=request.POST,
+ user=request.user
+ )
if form.is_valid():
ask_anonymously = form.cleaned_data['ask_anonymously']
title = form.cleaned_data['title']
@@ -116,7 +119,10 @@ def ask_widget(request, widget_id):
next_url = '%s?next=%s' % (reverse('widget_signin'), reverse('ask_by_widget'))
return redirect(next_url)
- form = forms.AskWidgetForm(include_text=widget.include_text_field)
+ form = forms.AskWidgetForm(
+ include_text=widget.include_text_field,
+ user=request.user
+ )
data = {
'form': form,
diff --git a/askbot/views/writers.py b/askbot/views/writers.py
index db7a24d2..d1504d23 100644
--- a/askbot/views/writers.py
+++ b/askbot/views/writers.py
@@ -211,7 +211,7 @@ def ask(request):#view used to ask a new question
user can start posting a question anonymously but then
must login/register in order for the question go be shown
"""
- form = forms.AskForm(request.REQUEST)
+ form = forms.AskForm(request.REQUEST, user=request.user)
if request.method == 'POST':
if form.is_valid():
timestamp = datetime.datetime.now()
@@ -264,7 +264,7 @@ def ask(request):#view used to ask a new question
return HttpResponseRedirect(url_utils.get_login_url())
if request.method == 'GET':
- form = forms.AskForm()
+ form = forms.AskForm(user=request.user)
draft_title = ''
draft_text = ''
@@ -386,24 +386,24 @@ def edit_question(request, id):
rev_id = revision_form.cleaned_data['revision']
revision = question.revisions.get(revision = rev_id)
form = forms.EditQuestionForm(
- question = question,
- user = request.user,
- revision = revision
+ question=question,
+ user=request.user,
+ revision=revision
)
else:
form = forms.EditQuestionForm(
request.POST,
- question = question,
- user = request.user,
- revision = revision
+ question=question,
+ user=quest.user,
+ revision=revision
)
else:#new content edit
# Always check modifications against the latest revision
form = forms.EditQuestionForm(
request.POST,
- question = question,
- revision = revision,
- user = request.user,
+ question=question,
+ revision=revision,
+ user=request.user,
)
revision_form = forms.RevisionForm(question, revision)
if form.is_valid():
@@ -437,10 +437,10 @@ def edit_question(request, id):
'wiki': question.wiki
}
form = forms.EditQuestionForm(
- question = question,
- revision = revision,
- user = request.user,
- initial = initial
+ question=question,
+ revision=revision,
+ user=request.user,
+ initial=initial
)
data = {
@@ -481,15 +481,20 @@ def edit_answer(request, id):
# Replace with those from the selected revision
rev = revision_form.cleaned_data['revision']
revision = answer.revisions.get(revision = rev)
- form = forms.EditAnswerForm(answer, revision)
+ form = forms.EditAnswerForm(
+ answer, revision, user=request.user
+ )
else:
form = forms.EditAnswerForm(
answer,
revision,
- request.POST
+ request.POST,
+ user=request.user
)
else:
- form = forms.EditAnswerForm(answer, revision, request.POST)
+ form = forms.EditAnswerForm(
+ answer, revision, request.POST, user=request.user
+ )
revision_form = forms.RevisionForm(answer, revision)
if form.is_valid():
@@ -506,7 +511,7 @@ def edit_answer(request, id):
return HttpResponseRedirect(answer.get_absolute_url())
else:
revision_form = forms.RevisionForm(answer, revision)
- form = forms.EditAnswerForm(answer, revision)
+ form = forms.EditAnswerForm(answer, revision, user=request.user)
if request.user.can_make_group_private_posts():
form.initial['post_privately'] = answer.is_private()
data = {
@@ -536,7 +541,7 @@ def answer(request, id):#process a new answer
"""
question = get_object_or_404(models.Post, post_type='question', id=id)
if request.method == "POST":
- form = forms.AnswerForm(request.POST)
+ form = forms.AnswerForm(request.POST, user=request.user)
if form.is_valid():
wiki = form.cleaned_data['wiki']
text = form.cleaned_data['text']