start package
authorOlivier Larchevêque <olivier.larcheveque@auf.org>
Thu, 8 Aug 2013 22:15:37 +0000 (18:15 -0400)
committerOlivier Larchevêque <olivier.larcheveque@auf.org>
Thu, 8 Aug 2013 22:15:37 +0000 (18:15 -0400)
15 files changed:
.gitignore
MANIFEST.in [new file with mode: 0644]
secretquestions/conf.py [new file with mode: 0644]
secretquestions/decorators.py [new file with mode: 0644]
secretquestions/forms.py
secretquestions/models.py
secretquestions/templates/base.html [new file with mode: 0644]
secretquestions/templates/secretquestions/step.html [new file with mode: 0644]
secretquestions/tests/__init__.py [new file with mode: 0644]
secretquestions/tests/configuration.py [new file with mode: 0644]
secretquestions/tests/settings.py [new file with mode: 0644]
secretquestions/tests/urls.py [new file with mode: 0644]
secretquestions/urls.py
secretquestions/views.py
tox.ini [new file with mode: 0644]

index 3ab10f0..95a52ec 100644 (file)
@@ -1,2 +1,3 @@
 *egg-info
 *.pyc
+.tox
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644 (file)
index 0000000..943440d
--- /dev/null
@@ -0,0 +1 @@
+recursive-include secretquestions/templates *
diff --git a/secretquestions/conf.py b/secretquestions/conf.py
new file mode 100644 (file)
index 0000000..289e263
--- /dev/null
@@ -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)
diff --git a/secretquestions/decorators.py b/secretquestions/decorators.py
new file mode 100644 (file)
index 0000000..6c5d84e
--- /dev/null
@@ -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
index 2a13145..2fa7c6e 100644 (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."))
index f567ed4..a373b4d 100644 (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)
 
diff --git a/secretquestions/templates/base.html b/secretquestions/templates/base.html
new file mode 100644 (file)
index 0000000..e5ba34e
--- /dev/null
@@ -0,0 +1,6 @@
+<html>
+    <body>
+    {% block content %}
+    {% endblock content %}
+    </body>
+</html>
diff --git a/secretquestions/templates/secretquestions/step.html b/secretquestions/templates/secretquestions/step.html
new file mode 100644 (file)
index 0000000..69a8dd0
--- /dev/null
@@ -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 %}
diff --git a/secretquestions/tests/__init__.py b/secretquestions/tests/__init__.py
new file mode 100644 (file)
index 0000000..c48b029
--- /dev/null
@@ -0,0 +1 @@
+from configuration import ConfigurationTest
diff --git a/secretquestions/tests/configuration.py b/secretquestions/tests/configuration.py
new file mode 100644 (file)
index 0000000..8e42c09
--- /dev/null
@@ -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)
+        
diff --git a/secretquestions/tests/settings.py b/secretquestions/tests/settings.py
new file mode 100644 (file)
index 0000000..30910d4
--- /dev/null
@@ -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',)
diff --git a/secretquestions/tests/urls.py b/secretquestions/tests/urls.py
new file mode 100644 (file)
index 0000000..12e9f41
--- /dev/null
@@ -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')),
+)
index 1dbcc54..eecfc63 100644 (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"),
 )
index 386db09..5d3784a 100644 (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)
diff --git a/tox.ini b/tox.ini
new file mode 100644 (file)
index 0000000..d4862e7
--- /dev/null
+++ b/tox.ini
@@ -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