start package

This commit is contained in:
Olivier Larchevêque 2013-08-08 18:15:37 -04:00
parent de5e37d266
commit de18189a9d
15 changed files with 317 additions and 9 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
*egg-info
*.pyc
.tox

1
MANIFEST.in Normal file
View File

@ -0,0 +1 @@
recursive-include secretquestions/templates *

6
secretquestions/conf.py Normal file
View File

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
from django.conf import settings
SQ_SESSION_KEY = getattr(settings, 'SQ_SESSION_KEY', 'sq_token')
SQ_TOKEN_TTL = getattr(settings, 'SQ_TOKEN_TTL', 60*3)

View File

@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
from datetime import timedelta, datetime
import re
from django.shortcuts import redirect
from django.utils.translation import ugettext as _
from django.contrib import messages
from .views import SecretQuestionWizard
from .conf import SQ_SESSION_KEY, SQ_TOKEN_TTL
def secret_questions_required(view, ttl=SQ_TOKEN_TTL):
def _wrapped(request, *args, **kwargs):
session_token, url, date = request.session.get(SQ_SESSION_KEY,
(None, None, datetime.now()))
get_token = request.GET.get(SQ_SESSION_KEY, None)
date_max = date + timedelta(seconds=ttl)
if session_token is None or get_token is None:
wiz = SecretQuestionWizard(request)
return wiz(request, *args, **kwargs)
if date_max < datetime.now() or \
not request.get_full_path().startswith(url):
if request.method == "POST":
messages.error(request, _("Your modifications were canceled."))
url = request.get_full_path()
clean_url = re.sub("(.*)%s=[a..z0..9]*(.*)" % SQ_SESSION_KEY, "\\1", url)
return redirect(clean_url)
if session_token == get_token:
return view(request, *args, **kwargs)
raise Exception('SQ')
return _wrapped

View File

@ -5,7 +5,9 @@ from django import forms
from django.forms.models import modelformset_factory, ModelForm
from django.utils.translation import ugettext as _
from .models import Answer, crypt_answer
from django.contrib.auth.models import User
from .models import Answer, crypt_answer, check_answer
MAX_SECRET_QUESTIONS = getattr(settings, 'MAX_SECRET_QUESTIONS', 3)
@ -62,3 +64,23 @@ class AnswerFormSet(_FreeAnswerFormSet):
questions.append(question)
return super(AnswerFormSet, self).clean()
class UsernameForm(forms.Form):
username = forms.CharField()
def clean_username(self):
data = self.cleaned_data['username']
try:
return User.objects.get(username=data)
except User.DoesNotExist:
raise forms.ValidationError(_("Username not found"))
class QuestionForm(forms.Form):
raw_answer = forms.CharField()
def clean_raw_answer(self):
data = self.cleaned_data['raw_answer']
if not check_answer(data, self.answer.secret):
raise forms.ValidationError(_("This answer is incorrect."))

View File

@ -2,7 +2,7 @@
from django.db import models
from django.contrib.auth.models import get_hexdigest
from django.contrib.auth.models import get_hexdigest, check_password
def crypt_answer(raw):
@ -13,6 +13,9 @@ def crypt_answer(raw):
return '%s$%s$%s' % (algo, salt, hsh)
def check_answer(raw, crypted):
return check_password(raw, crypted)
class Question(models.Model):
text = models.CharField(max_length=255)
@ -21,7 +24,7 @@ class Question(models.Model):
class Answer(models.Model):
user = models.ForeignKey('auth.User')
user = models.ForeignKey('auth.User', related_name="secret_answers")
question = models.ForeignKey('secretquestions.Question')
secret = models.CharField(max_length=255)

View File

@ -0,0 +1,6 @@
<html>
<body>
{% block content %}
{% endblock content %}
</body>
</html>

View File

@ -0,0 +1,24 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
{% if form.user %}
<p>{% trans "Step" %} {{ step }}/{{ step_count }}</p>
{% endif %}
<p>
{% if form.answer %}
{{ form.answer.question }}
{% endif %}
</p>
<form action="" method="post">{% csrf_token %}
<table>
{{ form }}
</table>
<input type="hidden" name="{{ step_field }}" value="{{ step0 }}" />
{{ previous_fields|safe }}
<input type="submit" value="{% trans "Next" %}">
</form>
{% endblock %}

View File

@ -0,0 +1 @@
from configuration import ConfigurationTest

View File

@ -0,0 +1,96 @@
# -*- coding: utf-8 -*-
from django.test import TestCase
from django.core.urlresolvers import reverse
from django.test.client import Client
from django.conf import settings
from django.contrib.auth.models import User
from secretquestions.models import Question, Answer
class ConfigurationTest(TestCase):
client = Client()
username = 'paul'
password = 'lemay'
def setUp(self):
self.create_user()
self.create_questions()
def create_user(self):
self.user = User.objects.create(username=self.username)
self.user.set_password(self.password)
self.user.save()
def create_questions(self):
self.question1 = Question.objects.create(text="question1")
self.question2 = Question.objects.create(text="question2")
self.question3 = Question.objects.create(text="question3")
self.questions = (self.question1, self.question2, self.question3)
def test_access_setup_questions_for_anonymous(self):
url = reverse('sq_setup')
response = self.client.get(url)
self.assertEqual(response.status_code, 302)
self.assertEqual('Location' in response, True)
self.assertEqual(settings.LOGIN_URL in response['Location'], True)
def test_access_setup_questions_for_authenticated(self):
self.assertEqual(self.client.login(username=self.username,
password=self.password), True)
url = reverse('sq_setup')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_setting_answer_for_one_question(self):
raw_password = 'xxx'
self.assertEqual(self.client.login(username=self.username,
password=self.password), True)
url = reverse('sq_setup')
data = {
'form-TOTAL_FORMS': u'1',
'form-INITIAL_FORMS': u'0',
'form-MAX_NUM_FORMS': u'',
'form-0-question': self.question1.id,
'form-0-secret': raw_password,
}
response = self.client.post(url, data)
self.assertEqual(response.status_code, 302)
answer = Answer.objects.get(user=self.user, question=self.question1)
self.assertNotEqual(answer.secret, raw_password)
self.assertNotEqual(answer.secret, '')
def test_setting_empty_answer_for_one_question(self):
raw_password = ''
self.assertEqual(self.client.login(username=self.username,
password=self.password), True)
url = reverse('sq_setup')
data = {
'form-TOTAL_FORMS': u'1',
'form-INITIAL_FORMS': u'0',
'form-MAX_NUM_FORMS': u'',
'form-0-question': self.question1.id,
'form-0-secret': raw_password,
}
response = self.client.post(url, data)
self.assertEqual(response.status_code, 200)
with self.assertRaises(Answer.DoesNotExist):
Answer.objects.get(user=self.user, question=self.question1)
def test_check_reset(self):
raw_password = 'xxx'
self.test_setting_answer_for_one_question()
url = reverse('sq_setup')
response = self.client.get(url)
self.assertFalse(raw_password in response.content)
answer = Answer.objects.get(user=self.user, question=self.question1)
self.assertFalse(answer.secret in response.content)

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
SECRET_KEY = 'secret'
ROOT_URLCONF = 'secretquestions.tests.urls'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:',
}
}
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.admin',
'registration',
'secretquestions',)

View File

@ -0,0 +1,11 @@
from django.conf.urls.defaults import patterns, include
from django.contrib import admin
admin.autodiscover()
urlpatterns = patterns('',
(r'^admin/(.*)', include(admin.site.urls)),
(r'^accounts/', include('registration.urls')),
(r'^secret/', include('secretquestions.urls')),
)

View File

@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
from django.conf.urls.defaults import patterns
from django.conf.urls.defaults import patterns, url
urlpatterns = patterns('',
(r'questions/setup$', 'secretquestions.views.setup_form'),
(r'questions/ask$','secretquestions.views.ask_form'),
url(r'questions/setup$', 'secretquestions.views.setup_form',
name="sq_setup"),
)

View File

@ -1,15 +1,24 @@
# -*- coding: utf-8 -*-
from datetime import datetime
from urlparse import urlparse, parse_qs
from urllib import urlencode
from django.core.exceptions import ObjectDoesNotExist
from django.template import RequestContext
from django.shortcuts import render_to_response, redirect
from django.conf import settings
from django.utils.translation import ugettext as _
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.contrib import messages
from django.contrib.formtools.wizard import FormWizard
from .forms import AnswerFormSet
from django.middleware.csrf import _get_new_csrf_key
from .forms import AnswerFormSet, UsernameForm, QuestionForm
from .conf import SQ_SESSION_KEY
@login_required
def setup_form(request):
@ -27,5 +36,56 @@ def setup_form(request):
context_instance=RequestContext(request))
def ask_form(request):
pass
class SecretQuestionWizard(FormWizard):
__name__ = 'SecretQuestionWizard' # fix for debugtoolbar introspection
def __init__(self, request):
self.user = None
self.redirect = request.get_full_path()
self.step_mapping = {}
if request.user.is_authenticated():
form_list = []
else:
if request.POST:
username = request.POST.get('0-username')
try:
self.user = User.objects.get(username=username)
except ObjectDoesNotExist:
pass
form_list = [UsernameForm, ]
if self.user:
for answer in self.user.secret_answers.all():
self.step_mapping[len(form_list)] = answer
form_list.append(QuestionForm)
super(SecretQuestionWizard, self).__init__(form_list)
def get_form(self, step, data=None):
answer = self.step_mapping.get(step, None)
form = super(SecretQuestionWizard, self).get_form(step, data)
form.user = self.user
form.answer = answer
return form
def get_template(self, step):
return 'secretquestions/step.html'
def done(self, request, form_list):
for form in form_list:
if not form.is_valid():
return self.redirect
token = _get_new_csrf_key()
path = urlparse(self.redirect).path
params = parse_qs(urlparse(self.redirect).query, keep_blank_values=True)
params[SQ_SESSION_KEY] = token
qs = urlencode(params)
url = "%s?%s" % (path, qs)
request.session[SQ_SESSION_KEY] = (token, path, datetime.now())
return redirect(url)

19
tox.ini Normal file
View File

@ -0,0 +1,19 @@
[tox]
envlist = django1.3, django1.4
[testenv]
deps =
django-registration==0.8
commands =
django-admin.py test secretquestions --settings=secretquestions.tests.settings
[testenv:django1.3]
deps =
{[testenv]deps}
django==1.3
[testenv:django1.4]
deps =
{[testenv]deps}
django==1.4