Skip to content

Commit

Permalink
Closes OSQA 175 and OSQA 318. Added a bunch of options to configure t…
Browse files Browse the repository at this point in the history
…he email validation process, and what a user is not allowed to do without validating the email.

git-svn-id: http://svn.osqa.net/svnroot/osqa/trunk@443 0cfe37f9-358a-4d5e-be75-b63607b5c754
  • Loading branch information
hernani committed Jun 24, 2010
1 parent deac5b0 commit 617e60b
Show file tree
Hide file tree
Showing 14 changed files with 226 additions and 107 deletions.
2 changes: 1 addition & 1 deletion forum/actions/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def repute_users(self):

def process_action(self):
hash = ValidationHash.objects.create_new(self.user, 'email', [self.user.email])
send_template_email([self.user], "auth/email_validation.html", {'validation_code': hash})
send_template_email([self.user], "auth/welcome_email.html", {'validation_code': hash})

def describe(self, viewer=None):
return _("%(user)s %(have_has)s joined the %(app_name)s Q&A community") % {
Expand Down
2 changes: 1 addition & 1 deletion forum/badges/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def award(cls, user, action, once=False):
trigger = isinstance(action, Action) and action or None

if not awarded:
AwardAction(user=user, node=node, ip=action.ip).save(data=dict(badge=cls.ondb, trigger=trigger))
AwardAction(user=user, node=node).save(data=dict(badge=cls.ondb, trigger=trigger))
except MultipleObjectsReturned:
if node:
logging.error('Found multiple %s badges awarded for user %s (%s)' % (self.name, user.username, user.id))
Expand Down
22 changes: 20 additions & 2 deletions forum/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,16 @@ def decorated(self, *args, **kwargs):

return decorated

def false_if_validation_required_to(item):
def decorator(fn):
def decorated(self, *args, **kwargs):
if item in settings.REQUIRE_EMAIL_VALIDATION_TO and not self.email_isvalid:
return False
else:
return fn(self, *args, **kwargs)
return decorated
return decorator

class User(BaseModel, DjangoUser):
is_approved = models.BooleanField(default=False)
email_isvalid = models.BooleanField(default=False)
Expand All @@ -116,7 +126,7 @@ def __unicode__(self):

@property
def is_siteowner(self):
#temporary thing, for now lets just assume that the site owner will always be the first user of the application
#todo: temporary thing, for now lets just assume that the site owner will always be the first user of the application
return self.id == 1

@property
Expand Down Expand Up @@ -191,6 +201,7 @@ def can_vote_up(self):
def can_vote_down(self):
return self.reputation >= int(settings.REP_TO_VOTE_DOWN)

@false_if_validation_required_to('flag')
def can_flag_offensive(self, post=None):
if post is not None and post.author == self:
return False
Expand All @@ -203,9 +214,10 @@ def can_view_offensive_flags(self, post=None):
return self.reputation >= int(settings.REP_TO_VIEW_FLAGS)

@true_if_is_super_or_staff
@false_if_validation_required_to('comment')
def can_comment(self, post):
return self == post.author or self.reputation >= int(settings.REP_TO_COMMENT
) or (post.__class__.__name__ == "Answer" and self == post.question.author)
) or (post.__class__.__name__ == "Answer" and self == post.question.author)

@true_if_is_super_or_staff
def can_like_comment(self, comment):
Expand Down Expand Up @@ -271,6 +283,12 @@ def can_delete_post(self, post):
def can_upload_files(self):
return self.reputation >= int(settings.REP_TO_UPLOAD)

def email_valid_and_can_ask(self):
return 'ask' not in settings.REQUIRE_EMAIL_VALIDATION_TO or self.email_isvalid

def email_valid_and_can_answer(self):
return 'answer' not in settings.REQUIRE_EMAIL_VALIDATION_TO or self.email_isvalid

def check_password(self, old_passwd):
self.__dict__.update(self.__class__.objects.filter(id=self.id).values('password')[0])
return DjangoUser.check_password(self, old_passwd)
Expand Down
18 changes: 18 additions & 0 deletions forum/settings/users.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from forms import CommaStringListWidget
from django.forms import CheckboxSelectMultiple
from base import Setting, SettingSet
from django.utils.translation import ugettext as _

Expand All @@ -24,3 +25,20 @@
label = _("Force unique email"),
help_text = _("Should each user have an unique email.")))

REQUIRE_EMAIL_VALIDATION_TO = Setting('REQUIRE_EMAIL_VALIDATION_TO', [], USERS_SET, dict(
label = _("Require email validation to..."),
help_text = _("Which actions in this site, users without a valid email will be prevented from doing."),
widget=CheckboxSelectMultiple,
choices=(("ask", _("ask questions")), ("answer", _("provide answers")), ("comment", _("make comments")), ("flag", _("report posts"))),
required=False,
))

HOLD_PENDING_POSTS_MINUTES = Setting('HOLD_PENDING_POSTS_MINUTES', 120, USERS_SET, dict(
label=_("Hold pending posts for X minutes"),
help_text=_("How much time in minutes a post should be kept in session until the user logs in or validates the email.")
))

WARN_PENDING_POSTS_MINUTES = Setting('WARN_PENDING_POSTS_MINUTES', 15, USERS_SET, dict(
label=_("Warn about pending posts afer X minutes"),
help_text=_("How much time in minutes a user that just logged in or validated his email should be warned about a pending post instead of publishing it automatically.")
))
22 changes: 12 additions & 10 deletions forum/skins/default/templates/ask.html
Original file line number Diff line number Diff line change
Expand Up @@ -71,18 +71,20 @@
<form id="fmask" action="" method="post" accept-charset="utf-8">
{% if not request.user.is_authenticated %}
<div class="message">
<p>{% trans "login to post question info" %}</p>
<span class="strong big">{% trans "You are welcome to start submitting your question anonymously." %}</span>
<p>{% blocktrans %}
After submiting your question, you will be redirected to the login/signup page.
Your question will be saved in the current session and will be published after you login with your existing account,
or signup for a new account{% endblocktrans %}{% if "ask" in settings.REQUIRE_EMAIL_VALIDATION_TO %}
{% trans "and validate your email." %}{% else %}.{% endif %}</p>
</div>
{% else %}
{% ifequal settings.EMAIL_VALIDATION 'on' %}
{% if not request.user.email_isvalid %}
<div class="message">
{% blocktrans with request.user.email as email %}must have valid {{email}} to post,
see {{email_validation_faq_url}}
{% endblocktrans %}
</div>
{% endif %}
{% endifequal %}
{% if not request.user.email_valid_and_can_ask %}
<div class="message">
{% blocktrans %}Remember, your question will not be published until you validate your email.{% endblocktrans %}
<a href="{% url send_validation_email %}">{% trans "Send me a validation link." %}</a>
</div>
{% endif %}
{% endif %}
<div class="form-item">
<label for="id_title" ><strong>{{ form.title.label_tag }}:</strong></label> <span class="form-error"></span><br/>
Expand Down
30 changes: 30 additions & 0 deletions forum/skins/default/templates/auth/mail_validation.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{% load i18n extra_tags email_tags %}

{% declare %}
prefix = settings.EMAIL_SUBJECT_PREFIX
app_name = settings.APP_SHORT_NAME

exclude_finetune = True
{% enddeclare %}

{% email %}
{% subject %}{% blocktrans %}{{ prefix }} Your email validation link {{ app_name }}{% endblocktrans %}{% endsubject %}

{% htmlcontent notifications/base.html %}
<p style="{{ p_style }}}">{% trans "Please use the following link to help us verify your email address:" %}</p>

<a style="{{ a_style }}}" href="{% fullurl auth_validate_email user=recipient.id,code=validation_code %}">{% trans "Validate my email address" %}</a>

<p style="{{ p_style }}}">{% trans "If the above link is not clickable, copy and paste this url into your web browser's address bar:" %}</p>

<p style="{{ p_style }}">{% fullurl auth_validate_email user=recipient.id,code=validation_code %}</p>
{% endhtmlcontent %}

{% textcontent notifications/base_text.html %}
{% trans "Copy and paste this url into your web browser's address bar to help us verify your email address:" %}

{% fullurl auth_validate_email user=recipient.id,code=validation_code %}
{% endtextcontent %}

{% endemail %}

10 changes: 7 additions & 3 deletions forum/skins/default/templates/question.html
Original file line number Diff line number Diff line change
Expand Up @@ -179,14 +179,18 @@ <h3>
{% endspaceless %}
</div>
{% if not request.user.is_authenticated %}
<div class="message">{% trans "you can answer anonymously and then login" %}</div>
<div class="message">{% trans "You can answer anonymously and then login." %}</div>
{% else %}
<p class="message">
{% ifequal request.user question.author %}
{% trans "answer your own question only to give an answer" %}
{% trans "Answer your own question only to give an answer." %}
{% else %}
{% trans "please only give an answer, no discussions" %}
{% trans "Please only give an answer, no discussions." %}
{% endifequal %}
{% if not request.user.email_valid_and_can_answer %}
{% blocktrans %}Remember, your answer will not be published until you validate your email.{% endblocktrans %}
<a href="{% url send_validation_email %}">{% trans "Send me a validation link." %}</a>
{% endif %}
</p>
{% endif %}

Expand Down
12 changes: 10 additions & 2 deletions forum/skins/default/templates/users/info.html
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,18 @@
{% endif %}
</tr>
{% endif %}
{% if request.user.is_superuser %}
{% if can_view_private %}
<tr>
<td>{% trans "email" %}</td>
<td><a href="mailto: {{ view_user.email }}">{{ view_user.email }}</a></td>
<td>
<a href="mailto: {{ view_user.email }}">{{ view_user.email }}</a>
{% if not view_user.email_isvalid %}
({% trans "not validated" %})
{% ifequal request.user view_user %}
</td></tr><tr><td></td><td><a href="{% url send_validation_email %}">{% trans "Send me a validation link." %}</a>
{% endifequal %}
{% endif %}
</td>
</tr>
{% endif %}
<!--
Expand Down
6 changes: 3 additions & 3 deletions forum/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@
kwargs=dict(close=True), name='close'),
url(r'^%s(?P<id>\d+)/%s$' % (_('questions/'), _('reopen/')), app.commands.close,
kwargs=dict(close=False), name='reopen'),
url(r'^%s(?P<id>\d+)/%s$' % (_('questions/'), _('answer/')), app.writers.answer, name='answer'),
url(r'^%s(?P<id>\d+)/%s$' % (_('questions/'), _('answer/')), app.writers.answer, name='answer'),
url(r'^%s(?P<action>\w+)/$' % _('pending-data/'), app.writers.manage_pending_data, name='manage_pending_data'),

url(r'^%s(?P<id>\d+)/(?P<vote_type>[a-z]+)/' % _('vote/'), app.commands.vote_post,
name='vote_post'),
Expand Down Expand Up @@ -144,8 +145,6 @@

url(r'^%s%s$' % (_('account/'), _('signin/')), app.auth.signin_page, name='auth_signin'),
url(r'^%s%s$' % (_('account/'), _('signout/')), app.auth.signout, name='user_signout'),
url(r'^%s%s(?P<action>\w+)/$' % (_('account/'), _('signin/')), app.auth.signin_page,
name='auth_action_signin'),
url(r'^%s(?P<provider>\w+)/%s$' % (_('account/'), _('signin/')),
app.auth.prepare_provider_signin, name='auth_provider_signin'),
url(r'^%s(?P<provider>\w+)/%s$' % (_('account/'), _('done/')), app.auth.process_provider_signin,
Expand All @@ -164,6 +163,7 @@
app.auth.remove_external_provider, name='user_remove_external_provider'),
url(r'^%s%s%s$' % (_('account/'), _('providers/'), _('add/')), app.auth.signin_page,
name='user_add_external_provider'),
url(r'^%s%s$' %(_('account/'), _('send-validation/')), app.auth.send_validation_email, name="send_validation_email"),


url(r'^%s$' % _('admin/'), app.admin.dashboard, name="admin_index"),
Expand Down
84 changes: 40 additions & 44 deletions forum/views/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
from django.contrib.auth import login, logout
from django.http import get_host
from forum.actions import SuspendAction
from forum.utils import html
from forum import settings
from writers import manage_pending_data
import types
import datetime
import logging
Expand All @@ -25,12 +28,8 @@
from forum.models import AuthKeyUserAssociation, ValidationHash, Question, Answer
from forum.actions import UserJoinsAction

def signin_page(request, action=None):
if action is None:
request.session['on_signin_url'] = request.META.get('HTTP_REFERER', '/')
else:
request.session['on_signin_action'] = action
request.session['on_signin_url'] = reverse('auth_action_signin', kwargs={'action': action})
def signin_page(request):
request.session['on_signin_url'] = request.META.get('HTTP_REFERER', '/')

all_providers = [provider.context for provider in AUTH_PROVIDERS.values()]

Expand Down Expand Up @@ -263,6 +262,24 @@ def temp_signin(request, user, code):
else:
raise Http404()

def send_validation_email(request):
if not request.user.is_authenticated():
return HttpResponseUnauthorized(request)
else:
try:
hash = ValidationHash.objects.get(user=request.user, type='email')
if hash.expiration < datetime.datetime.now():
hash.delete()
return send_validation_email(request)
except:
hash = ValidationHash.objects.create_new(request.user, 'email', [request.user.email])

send_template_email([request.user], "auth/mail_validation.html", {'validation_code': hash})
request.user.message_set.create(message=_("A message with an email validation link was just sent to your address."))
return HttpResponseRedirect(request.META.get('HTTP_REFERER', '/'))



def validate_email(request, user, code):
user = get_object_or_404(User, id=user)

Expand Down Expand Up @@ -338,55 +355,34 @@ def remove_external_provider(request, id):
association.delete()
return HttpResponseRedirect(reverse('user_authsettings', kwargs={'id': association.user.id}))

def newquestion_signin_action(user):
question = Question.objects.filter(author=user).order_by('-added_at')[0]
return question.get_absolute_url()

def newanswer_signin_action(user):
answer = Answer.objects.filter(author=user).order_by('-added_at')[0]
return answer.get_absolute_url()

POST_SIGNIN_ACTIONS = {
'newquestion': newquestion_signin_action,
'newanswer': newanswer_signin_action,
}

def login_and_forward(request, user, forward=None, message=None):
if user.is_suspended():
return forward_suspended_user(request, user)

user.backend = "django.contrib.auth.backends.ModelBackend"
login(request, user)

temp_data = request.session.pop('temp_node_data', None)
if temp_data:
request.POST = temp_data
node_type = request.session.pop('temp_node_type')

if node_type == "question":
from forum.views.writers import ask
return ask(request)
elif node_type == "answer":
from forum.views.writers import answer
return answer(request, request.session.pop('temp_question_id'))

if not forward:
signin_action = request.session.get('on_signin_action', None)
if not signin_action:
forward = request.session.get('on_signin_url', None)

if not forward:
forward = reverse('index')
else:
try:
forward = POST_SIGNIN_ACTIONS[signin_action](user)
except:
forward = reverse('index')

if message is None:
message = _("Welcome back %s, you are now logged in") % user.username

request.user.message_set.create(message=message)

forward = request.session.get('on_signin_url', reverse('index'))
pending_data = request.session.get('pending_submission_data', None)

if pending_data and (user.email_isvalid or pending_data['type'] not in settings.REQUIRE_EMAIL_VALIDATION_TO):
submission_time = pending_data['time']
if submission_time < datetime.datetime.now() - datetime.timedelta(minutes=int(settings.HOLD_PENDING_POSTS_MINUTES)):
del request.session['pending_submission_data']
elif submission_time < datetime.datetime.now() - datetime.timedelta(minutes=int(settings.WARN_PENDING_POSTS_MINUTES)):
user.message_set.create(message=(_("You have a %s pending submission.") % pending_data['data_name']) + " %s, %s, %s" % (
html.hyperlink(reverse('manage_pending_data', kwargs={'action': _('save')}), _("save it")),
html.hyperlink(reverse('manage_pending_data', kwargs={'action': _('review')}), _("review")),
html.hyperlink(reverse('manage_pending_data', kwargs={'action': _('cancel')}), _("cancel"))
))
else:
return manage_pending_data(request, _('save'), forward)

return HttpResponseRedirect(forward)

def forward_suspended_user(request, user, show_private_msg=True):
Expand Down
12 changes: 4 additions & 8 deletions forum/views/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,6 @@ def users(request):
}
}

def set_new_email(user, new_email, nomessage=False):
if new_email != user.email:
user.email = new_email
user.email_isvalid = False
user.save()
#if settings.EMAIL_VALIDATION == 'on':
# send_new_email_key(user,nomessage=nomessage)

@login_required
def edit_user(request, id):
Expand All @@ -93,7 +86,9 @@ def edit_user(request, id):
if form.is_valid():
new_email = sanitize_html(form.cleaned_data['email'])

set_new_email(user, new_email)
if new_email != user.email:
user.email = new_email
user.email_isvalid = False

if settings.EDITABLE_SCREEN_NAME:
user.username = sanitize_html(form.cleaned_data['username'])
Expand All @@ -108,6 +103,7 @@ def edit_user(request, id):
user.save()
EditProfileAction(user=user, ip=request.META['REMOTE_ADDR']).save()

request.user.message_set.create(message=_("Profile updated."))
return HttpResponseRedirect(user.get_profile_url())
else:
form = EditUserForm(user)
Expand Down
Loading

0 comments on commit 617e60b

Please sign in to comment.