rename package finish testing
authorOlivier Larchevêque <olivier.larcheveque@auf.org>
Mon, 12 Aug 2013 19:13:07 +0000 (15:13 -0400)
committerOlivier Larchevêque <olivier.larcheveque@auf.org>
Mon, 12 Aug 2013 19:13:07 +0000 (15:13 -0400)
46 files changed:
MANIFEST.in
auf/__init__.py [new file with mode: 0644]
auf/django/__init__.py [new file with mode: 0644]
auf/django/secretquestions/__init__.py [new file with mode: 0644]
auf/django/secretquestions/admin.py [new file with mode: 0644]
auf/django/secretquestions/conf.py [new file with mode: 0644]
auf/django/secretquestions/decorators.py [new file with mode: 0644]
auf/django/secretquestions/forms.py [new file with mode: 0644]
auf/django/secretquestions/migrations/0001_initial.py [new file with mode: 0644]
auf/django/secretquestions/migrations/__init__.py [new file with mode: 0644]
auf/django/secretquestions/models.py [new file with mode: 0644]
auf/django/secretquestions/templates/base.html [new file with mode: 0644]
auf/django/secretquestions/templates/secretquestions/setup_form.html [new file with mode: 0644]
auf/django/secretquestions/templates/secretquestions/step.html [new file with mode: 0644]
auf/django/secretquestions/tests/__init__.py [new file with mode: 0644]
auf/django/secretquestions/tests/common.py [new file with mode: 0644]
auf/django/secretquestions/tests/configuration.py [new file with mode: 0644]
auf/django/secretquestions/tests/settings.py [new file with mode: 0644]
auf/django/secretquestions/tests/token.py [new file with mode: 0644]
auf/django/secretquestions/tests/urls.py [new file with mode: 0644]
auf/django/secretquestions/tests/use.py [new file with mode: 0644]
auf/django/secretquestions/tests/views.py [new file with mode: 0644]
auf/django/secretquestions/urls.py [new file with mode: 0644]
auf/django/secretquestions/views.py [new file with mode: 0644]
secretquestions/__init__.py [deleted file]
secretquestions/admin.py [deleted file]
secretquestions/conf.py [deleted file]
secretquestions/decorators.py [deleted file]
secretquestions/forms.py [deleted file]
secretquestions/migrations/0001_initial.py [deleted file]
secretquestions/migrations/__init__.py [deleted file]
secretquestions/models.py [deleted file]
secretquestions/templates/base.html [deleted file]
secretquestions/templates/secretquestions/setup_form.html [deleted file]
secretquestions/templates/secretquestions/step.html [deleted file]
secretquestions/tests/__init__.py [deleted file]
secretquestions/tests/common.py [deleted file]
secretquestions/tests/configuration.py [deleted file]
secretquestions/tests/settings.py [deleted file]
secretquestions/tests/urls.py [deleted file]
secretquestions/tests/use.py [deleted file]
secretquestions/tests/views.py [deleted file]
secretquestions/urls.py [deleted file]
secretquestions/views.py [deleted file]
setup.py
tox.ini

index 943440d..1bf6e0d 100644 (file)
@@ -1 +1 @@
-recursive-include secretquestions/templates *
+recursive-include auf/django/secretquestions/templates *
diff --git a/auf/__init__.py b/auf/__init__.py
new file mode 100644 (file)
index 0000000..ece379c
--- /dev/null
@@ -0,0 +1,2 @@
+import pkg_resources
+pkg_resources.declare_namespace(__name__)
diff --git a/auf/django/__init__.py b/auf/django/__init__.py
new file mode 100644 (file)
index 0000000..ece379c
--- /dev/null
@@ -0,0 +1,2 @@
+import pkg_resources
+pkg_resources.declare_namespace(__name__)
diff --git a/auf/django/secretquestions/__init__.py b/auf/django/secretquestions/__init__.py
new file mode 100644 (file)
index 0000000..792d600
--- /dev/null
@@ -0,0 +1 @@
+#
diff --git a/auf/django/secretquestions/admin.py b/auf/django/secretquestions/admin.py
new file mode 100644 (file)
index 0000000..f1c282b
--- /dev/null
@@ -0,0 +1,7 @@
+# -*- coding: utf-8 -*-
+
+from django.contrib import admin
+
+from .models import Question
+
+admin.site.register(Question)
diff --git a/auf/django/secretquestions/conf.py b/auf/django/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/auf/django/secretquestions/decorators.py b/auf/django/secretquestions/decorators.py
new file mode 100644 (file)
index 0000000..7e16777
--- /dev/null
@@ -0,0 +1,48 @@
+# -*- 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(ttl=SQ_TOKEN_TTL):
+
+    def _inner(view):
+
+        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()
+                regex_no_session_key = "(.*)%s=[a..z0..9]*(.*)" % \
+                    SQ_SESSION_KEY
+                clean_url = re.sub(regex_no_session_key, "\\1", url)
+                return redirect(clean_url)
+
+            if session_token == get_token:
+                return view(request, *args, **kwargs)
+
+            # should not be raised
+            raise Exception('SQ')   # pragma: no cover
+        return _wrapped
+    return _inner
diff --git a/auf/django/secretquestions/forms.py b/auf/django/secretquestions/forms.py
new file mode 100644 (file)
index 0000000..1bcf3e8
--- /dev/null
@@ -0,0 +1,87 @@
+# -*- coding: utf-8 -*-
+
+from django.conf import settings
+from django import forms
+from django.forms.models import modelformset_factory, ModelForm
+from django.utils.translation import ugettext as _
+
+from django.contrib.auth.models import User
+
+from .models import Answer, crypt_answer, check_answer
+
+
+MAX_SECRET_QUESTIONS = getattr(settings, 'MAX_SECRET_QUESTIONS', 3)
+
+
+class AnswerForm(ModelForm):
+
+    class Meta:
+        model = Answer
+
+    def __init__(self, *args, **kwargs):
+        if 'instance' in kwargs:
+            kwargs['instance'].secret = ""
+        super(AnswerForm, self).__init__(*args, **kwargs)
+
+    def clean_secret(self):
+        data = self.cleaned_data['secret']
+        return crypt_answer(data)
+
+
+_FreeAnswerFormSet = modelformset_factory(Answer,
+                                          form=AnswerForm,
+                                          fields=("question", "secret"),
+                                          extra=MAX_SECRET_QUESTIONS,
+                                          max_num=MAX_SECRET_QUESTIONS,
+                                          can_delete=False)
+
+
+class AnswerFormSet(_FreeAnswerFormSet):
+
+    def __init__(self, *args, **kwargs):
+        self.user = kwargs.pop('user')
+        super(AnswerFormSet, self).__init__(*args, **kwargs)
+
+    def save_all(self):
+        instances = self.save(commit=False)
+        for instance in instances:
+            instance.user = self.user
+            instance.save()
+
+    def clean(self):
+        questions = []
+        for i in range(0, self.total_form_count()):
+            form = self.forms[i]
+            try:
+                question = form.cleaned_data.get('question')
+            except:
+                question = None
+            if question is None:
+                error_msg = _("All questions have to be selected.")
+                raise forms.ValidationError(error_msg)
+            if question in questions:
+                error_msg = _("Each question has to be different.")
+                raise forms.ValidationError(error_msg)
+            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."))
diff --git a/auf/django/secretquestions/migrations/0001_initial.py b/auf/django/secretquestions/migrations/0001_initial.py
new file mode 100644 (file)
index 0000000..02ce945
--- /dev/null
@@ -0,0 +1,87 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        # Adding model 'Question'
+        db.create_table('secretquestions_question', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('text', self.gf('django.db.models.fields.CharField')(max_length=255)),
+        ))
+        db.send_create_signal('secretquestions', ['Question'])
+
+        # Adding model 'Answer'
+        db.create_table('secretquestions_answer', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
+            ('question', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['secretquestions.Question'])),
+            ('secret', self.gf('django.db.models.fields.CharField')(max_length=255)),
+        ))
+        db.send_create_signal('secretquestions', ['Answer'])
+
+
+    def backwards(self, orm):
+        # Deleting model 'Question'
+        db.delete_table('secretquestions_question')
+
+        # Deleting model 'Answer'
+        db.delete_table('secretquestions_answer')
+
+
+    models = {
+        'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        'auth.permission': {
+            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        'auth.user': {
+            'Meta': {'object_name': 'User'},
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        'contenttypes.contenttype': {
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        'secretquestions.answer': {
+            'Meta': {'object_name': 'Answer'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['secretquestions.Question']"}),
+            'secret': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+        },
+        'secretquestions.question': {
+            'Meta': {'object_name': 'Question'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'text': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        }
+    }
+
+    complete_apps = ['secretquestions']
\ No newline at end of file
diff --git a/auf/django/secretquestions/migrations/__init__.py b/auf/django/secretquestions/migrations/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/auf/django/secretquestions/models.py b/auf/django/secretquestions/models.py
new file mode 100644 (file)
index 0000000..61a9353
--- /dev/null
@@ -0,0 +1,33 @@
+# -*- coding: utf-8 -*-
+
+from django.db import models
+
+from django.contrib.auth.models import get_hexdigest, check_password
+
+
+def crypt_answer(raw):
+    import random
+    algo = 'sha1'
+    salt = get_hexdigest(algo, str(random.random()), str(random.random()))[:5]
+    hsh = get_hexdigest(algo, salt, 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)
+
+    def __unicode__(self):
+        return self.text
+
+
+class Answer(models.Model):
+    user = models.ForeignKey('auth.User', related_name="secret_answers")
+    question = models.ForeignKey('secretquestions.Question')
+    secret = models.CharField(max_length=255)
+
+    class Meta:
+        unique_together = ('question', 'user')
diff --git a/auf/django/secretquestions/templates/base.html b/auf/django/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/auf/django/secretquestions/templates/secretquestions/setup_form.html b/auf/django/secretquestions/templates/secretquestions/setup_form.html
new file mode 100644 (file)
index 0000000..c76e9d6
--- /dev/null
@@ -0,0 +1,23 @@
+{% extends "base.html" %}
+{% load i18n %}
+
+{% block content %}
+
+<p>
+{% blocktrans %}
+Select several questions and register your secret answers.
+These one will be asked to you for some critical features which required more
+security to ensure that is you.
+{% endblocktrans %}
+</p>
+
+{{ formset.non_form_errors.as_ul }}
+
+<form action="" method="post">{% csrf_token %}
+    <table>
+        {{ formset }}
+    </table>
+    <input type="submit" value="{% trans "Save all" %}" />
+</form>
+
+{% endblock content %}
diff --git a/auf/django/secretquestions/templates/secretquestions/step.html b/auf/django/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/auf/django/secretquestions/tests/__init__.py b/auf/django/secretquestions/tests/__init__.py
new file mode 100644 (file)
index 0000000..acfcacb
--- /dev/null
@@ -0,0 +1,3 @@
+from .configuration import ConfigurationTest
+from .use import UseTest
+from .token import TokenTest
diff --git a/auf/django/secretquestions/tests/common.py b/auf/django/secretquestions/tests/common.py
new file mode 100644 (file)
index 0000000..937721c
--- /dev/null
@@ -0,0 +1,92 @@
+# -*- coding: utf-8 -*-
+
+import re
+
+from django.test import TestCase
+from django.test.client import Client
+
+from django.contrib.auth.models import User
+
+from auf.django.secretquestions.models import Question, Answer, crypt_answer
+
+
+class SecretQuestionTest(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.question1.save()
+        self.question2 = Question.objects.create(text="question2")
+        self.question2.save()
+
+    def create_answers(self):
+        self.answer1 = Answer.objects.create(question=self.question1,
+                                             secret=crypt_answer('one'),
+                                             user=self.user)
+        self.answer1.save()
+        self.answer2 = Answer.objects.create(question=self.question2,
+                                             secret=crypt_answer('two'),
+                                             user=self.user)
+        self.answer2.save()
+
+    def _get_hashs(self, response):
+        """
+        Parse response to prepare POST according previous hash
+        """
+        regex_hash = 'name="(hash_[0-9])+" value="([a-z0-9]+)"'
+        found = re.findall(regex_hash, response.content)
+        hashs = {}
+        for k, v in found:
+            hashs.update({k: v})
+        return hashs
+
+    def get_response_from_final_step(self, url):
+        self.create_answers()
+
+        response = self.client.get(url)
+        self.assertEqual(response.status_code, 200)
+        data = {'wizard_step': 0,
+                '0-username': self.username, }
+        response = self.client.post(url, data)
+        self.assertEqual(response.status_code, 200)
+        self.assertTrue(self.question1.text in response.content)
+
+        # wrong response
+        data = {'wizard_step': 1,
+                '0-username': self.username,
+                '1-raw_answer': 'wrong answer', }
+        data.update(self._get_hashs(response))
+        response = self.client.post(url, data)
+
+        self.assertEqual(response.status_code, 200)
+        self.assertFalse('2-raw_answer' in response.content)
+
+        # good response
+        data = {'wizard_step': 1,
+                '0-username': self.username,
+                '1-raw_answer': 'one', }
+        data.update(self._get_hashs(response))
+        response = self.client.post(url, data)
+        self.assertEqual(response.status_code, 200)
+        self.assertTrue('2-raw_answer' in response.content)
+
+        # good response
+        data = {'wizard_step': 2,
+                '0-username': self.username,
+                '1-raw_answer': 'one',
+                '2-raw_answer': 'two', }
+        data.update(self._get_hashs(response))
+        response = self.client.post(url, data)
+        return response
diff --git a/auf/django/secretquestions/tests/configuration.py b/auf/django/secretquestions/tests/configuration.py
new file mode 100644 (file)
index 0000000..943b840
--- /dev/null
@@ -0,0 +1,114 @@
+# -*- coding: utf-8 -*-
+
+from django.core.urlresolvers import reverse
+from django.conf import settings
+
+from auf.django.secretquestions.models import Answer
+
+from .common import SecretQuestionTest
+
+
+class ConfigurationTest(SecretQuestionTest):
+    """
+    TestCase for setting questions/answers
+    """
+
+    def test_access_setup_questions_for_anonymous(self):
+        """
+        Check if you try to access setup page as anonymous, you're redirected
+        to login page.
+        """
+        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):
+        """
+        Check if setup page is accessible from authenticated people
+        """
+        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):
+        """
+        Check if the answer is really stored and crypted
+        """
+        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_2_same_questions(self):
+        """
+        Check if error is raised if you choose 2 same questions
+        """
+        raw_password = 'xxx'
+        self.assertEqual(self.client.login(username=self.username,
+                                           password=self.password), True)
+        url = reverse('sq_setup')
+
+        data = {'form-TOTAL_FORMS': u'2',
+                'form-INITIAL_FORMS': u'0',
+                'form-MAX_NUM_FORMS': u'',
+                'form-0-question': self.question1.id,
+                'form-0-secret': raw_password,
+                'form-1-question': self.question1.id,
+                'form-1-secret': raw_password, }
+
+        response = self.client.post(url, data)
+        self.assertEqual(response.status_code, 200)
+        answers = Answer.objects.filter(user=self.user)
+        self.assertEqual(len(answers), 0)
+        self.assertTrue(
+            "Each question has to be different." in response.content)
+
+    def test_setting_empty_answer_for_one_question(self):
+        """
+        Check if the answer is not empty
+        """
+        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):
+        """
+        Check if you have ever set answer, the form does not prepopulate
+        this one.
+        """
+        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/auf/django/secretquestions/tests/settings.py b/auf/django/secretquestions/tests/settings.py
new file mode 100644 (file)
index 0000000..1931313
--- /dev/null
@@ -0,0 +1,17 @@
+# -*- coding: utf-8 -*-
+
+USE_I18N = False
+
+SECRET_KEY = 'secret'
+
+ROOT_URLCONF = 'auf.django.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',
+                  'auf.django.secretquestions',)
diff --git a/auf/django/secretquestions/tests/token.py b/auf/django/secretquestions/tests/token.py
new file mode 100644 (file)
index 0000000..8c6885e
--- /dev/null
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+
+
+import time
+
+from django.core.urlresolvers import reverse
+
+from .common import SecretQuestionTest
+from .views import TTL
+
+
+class TokenTest(SecretQuestionTest):
+    """
+    TestCase for token testing
+    """
+
+    def test_expiracy(self):
+        """
+        Check if you try to access page after X times.
+        """
+        url = reverse('sq_test_private_ttl')
+        response = self.get_response_from_final_step(url)
+        location = response['location']
+
+        response = self.client.get(location)
+        self.assertEqual(response.status_code, 200)
+        self.assertTrue('OK' in response.content)
+        time.sleep(TTL+1)
+        response = self.client.get(location)
+        self.assertEqual(response.status_code, 302)
+        redirect = response['location'].replace('http://testserver', '')
+        self.assertEqual(redirect, url)
+
+    def test_warning(self):
+        """
+        Check if you try to access page after X times.
+        """
+        url = reverse('sq_test_private_ttl')
+        response = self.get_response_from_final_step(url)
+        location = response['location']
+
+        response = self.client.get(location)
+        self.assertEqual(response.status_code, 200)
+        self.assertTrue('OK' in response.content)
+        time.sleep(TTL+1)
+        response = self.client.post(location, {})
+        self.assertEqual(response.status_code, 302)
+        redirect = response['location'].replace('http://testserver', '')
+        self.assertEqual(redirect, url)
+        response = self.client.get(url)
+        messages = [m.message for m in response.context['messages']]
+        self.assertTrue("Your modifications were canceled." in messages)
diff --git a/auf/django/secretquestions/tests/urls.py b/auf/django/secretquestions/tests/urls.py
new file mode 100644 (file)
index 0000000..7609c90
--- /dev/null
@@ -0,0 +1,21 @@
+from django.conf.urls.defaults import patterns, include, url
+
+from django.contrib import admin
+
+admin.autodiscover()
+
+urlpatterns = patterns(
+    '',
+    (r'^admin/(.*)', include(admin.site.urls)),
+    (r'^accounts/', include('registration.urls')),
+    (r'^secret/',
+        include('auf.django.secretquestions.urls')),
+    url(r'^test_public/',
+        'auf.django.secretquestions.tests.views.public',
+        name='sq_test_public'),
+    url(r'^test_private/',
+        'auf.django.secretquestions.tests.views.private',
+        name='sq_test_private'),
+    url(r'^test_private_ttl/',
+        'auf.django.secretquestions.tests.views.private_ttl',
+        name='sq_test_private_ttl'),)
diff --git a/auf/django/secretquestions/tests/use.py b/auf/django/secretquestions/tests/use.py
new file mode 100644 (file)
index 0000000..226f254
--- /dev/null
@@ -0,0 +1,166 @@
+# -*- coding: utf-8 -*-
+
+from django.core.urlresolvers import reverse
+
+from auf.django.secretquestions import conf
+
+from .common import SecretQuestionTest
+
+
+class UseTest(SecretQuestionTest):
+    """
+    TestCase for accessing guarded pages.
+    """
+
+    def test_access_public_for_anonymous(self):
+        """
+        Check if you try to access public page as anonymous
+        """
+        url = reverse('sq_test_public')
+        response = self.client.get(url)
+        self.assertEqual(response.status_code, 200)
+        self.assertTrue('OK' in response.content)
+
+    def test_access_public_for_authenticated(self):
+        """
+        Check if you try to access public page as authenticated
+        """
+        self.assertEqual(self.client.login(username=self.username,
+                                           password=self.password), True)
+        url = reverse('sq_test_public')
+        response = self.client.get(url)
+        self.assertEqual(response.status_code, 200)
+        self.assertTrue('OK' in response.content)
+
+    def test_access_private_for_anonymous(self):
+        """
+        Check if you try to access private page as anonymous,
+        you are asking for username
+        """
+        url = reverse('sq_test_private')
+        response = self.client.get(url)
+        self.assertEqual(response.status_code, 200)
+        self.assertFalse('OK' in response.content)
+        self.assertTrue('username' in response.content)
+
+    def test_access_private_for_authenticated(self):
+        """
+        Check if you try to access private page as authenticated,
+        you are asking for first question
+        """
+        self.create_answers()
+        self.assertEqual(self.client.login(username=self.username,
+                                           password=self.password), True)
+        url = reverse('sq_test_private')
+        response = self.client.get(url)
+        self.assertEqual(response.status_code, 200)
+        self.assertFalse('OK' in response.content)
+        self.assertFalse('username' in response.content)
+
+    def test_access_private_without_question(self):
+        """
+        Check if you try to access private page as authenticated,
+        you don't have set any secret questions yet
+        you are redirected to the page
+        """
+        self.assertEqual(self.client.login(username=self.username,
+                                           password=self.password), True)
+        url = reverse('sq_test_private')
+        response = self.client.get(url)
+        self.assertEqual(response.status_code, 302)
+        self.assertFalse('OK' in response.content)
+        self.assertFalse('username' in response.content)
+        self.assertTrue(url in response['location'])
+        self.assertTrue(conf.SQ_SESSION_KEY in response['location'])
+
+    def test_username_form(self):
+        """
+        Test form submission
+        """
+        url = reverse('sq_test_private')
+        response = self.client.get(url)
+        self.assertEqual(response.status_code, 200)
+        data = {'0-username': '', }
+        response = self.client.post(url, data)
+        self.assertTrue('This field is required' in response.content)
+        data = {'0-username': 'fake', }
+        response = self.client.post(url, data)
+        self.assertTrue('Username not found' in response.content)
+        data = {'0-username': self.username, }
+        response = self.client.post(url, data)
+        self.assertEqual(response.status_code, 302)
+        self.assertTrue(url in response['location'])
+        self.assertTrue(conf.SQ_SESSION_KEY in response['location'])
+
+    def test_question_form(self):
+        """
+        Test form submission
+        """
+        self.create_answers()
+        url = reverse('sq_test_private')
+        response = self.client.get(url)
+        self.assertEqual(response.status_code, 200)
+        data = {'wizard_step': 0,
+                '0-username': self.username, }
+        response = self.client.post(url, data)
+        self.assertEqual(response.status_code, 200)
+        self.assertTrue(self.question1.text in response.content)
+
+        # wrong response
+        data = {'wizard_step': 1,
+                '0-username': self.username,
+                '1-raw_answer': 'wrong answer', }
+        data.update(self._get_hashs(response))
+        response = self.client.post(url, data)
+
+        self.assertEqual(response.status_code, 200)
+        self.assertFalse('2-raw_answer' in response.content)
+
+        # good response
+        data = {'wizard_step': 1,
+                '0-username': self.username,
+                '1-raw_answer': 'one', }
+        data.update(self._get_hashs(response))
+        response = self.client.post(url, data)
+        self.assertEqual(response.status_code, 200)
+        self.assertTrue('2-raw_answer' in response.content)
+
+        # good response
+        data = {'wizard_step': 2,
+                '0-username': self.username,
+                '1-raw_answer': 'one',
+                '2-raw_answer': 'two', }
+        data.update(self._get_hashs(response))
+        response = self.client.post(url, data)
+        self.assertEqual(response.status_code, 302)
+        self.assertTrue(url in response['location'])
+        self.assertTrue(conf.SQ_SESSION_KEY in response['location'])
+
+        response = self.client.get(response['location'])
+        self.assertTrue('OK' in response.content)
+
+    def test_inconsistent_data(self):
+        """
+        Test inconsistent data between steps
+        wizard toolkit redirect on step with corrupted data
+        """
+        self.create_answers()
+        url = reverse('sq_test_private')
+        response = self.client.get(url)
+        data = {'wizard_step': 0,
+                '0-username': self.username, }
+        response = self.client.post(url, data)
+        self.assertTrue('1-raw_answer' in response.content)
+        data = {'wizard_step': 1,
+                '0-username': self.username,
+                '1-raw_answer': 'one', }
+        data.update(self._get_hashs(response))
+        response = self.client.post(url, data)
+        self.assertTrue('2-raw_answer' in response.content)
+        data = {'wizard_step': 2,
+                '0-username': self.username,
+                '1-raw_answer': 'xxx',
+                '2-raw_answer': 'two', }
+        data.update(self._get_hashs(response))
+        response = self.client.post(url, data)
+        self.assertTrue('1-raw_answer' in response.content)
diff --git a/auf/django/secretquestions/tests/views.py b/auf/django/secretquestions/tests/views.py
new file mode 100644 (file)
index 0000000..5d97413
--- /dev/null
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+
+from django.http import HttpResponse
+
+from auf.django.secretquestions.decorators import secret_questions_required
+
+TTL = 1
+
+
+def public(request):
+    return HttpResponse("OK")
+
+
+@secret_questions_required()
+def private(request):
+    return HttpResponse("OK")
+
+
+@secret_questions_required(ttl=TTL)
+def private_ttl(request):
+    return HttpResponse("OK")
diff --git a/auf/django/secretquestions/urls.py b/auf/django/secretquestions/urls.py
new file mode 100644 (file)
index 0000000..9d19b1b
--- /dev/null
@@ -0,0 +1,8 @@
+# -*- coding: utf-8 -*-
+
+from django.conf.urls.defaults import patterns, url
+
+urlpatterns = patterns('',
+                       url(r'questions/setup$',
+                           'auf.django.secretquestions.views.setup_form',
+                           name="sq_setup"), )
diff --git a/auf/django/secretquestions/views.py b/auf/django/secretquestions/views.py
new file mode 100644 (file)
index 0000000..e638095
--- /dev/null
@@ -0,0 +1,101 @@
+# -*- 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 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):
+    if request.method == 'POST':
+        formset = AnswerFormSet(request.POST, user=request.user)
+        if formset.is_valid():
+            formset.save_all()
+            messages.info(request,
+                          _("Your secret answers were successfully saved."))
+            return redirect(settings.LOGIN_REDIRECT_URL)
+    else:
+        formset = AnswerFormSet(user=request.user)
+    return render_to_response("secretquestions/setup_form.html", {
+        "formset": formset,
+        },
+        context_instance=RequestContext(request))
+
+
+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():
+            self.user = request.user
+            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):
+        # double check validation, it's already done
+        # by wizard and redirect on the step with corrupted data
+        for form in form_list:
+            if not form.is_valid():
+                raise Exception('SQ corrupted data')  # pragma: no cover
+
+        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)
+
+    def __call__(self, request, *args, **kwargs):
+        if len(self.form_list) == 0:
+            return self.done(request, self.form_list)
+        return super(SecretQuestionWizard, self).\
+            __call__(request, *args, **kwargs)
diff --git a/secretquestions/__init__.py b/secretquestions/__init__.py
deleted file mode 100644 (file)
index 792d600..0000000
+++ /dev/null
@@ -1 +0,0 @@
-#
diff --git a/secretquestions/admin.py b/secretquestions/admin.py
deleted file mode 100644 (file)
index f1c282b..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from django.contrib import admin
-
-from .models import Question
-
-admin.site.register(Question)
diff --git a/secretquestions/conf.py b/secretquestions/conf.py
deleted file mode 100644 (file)
index 289e263..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-# -*- 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
deleted file mode 100644 (file)
index 9bbd99f..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-# -*- 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()
-            regex_no_session_key = "(.*)%s=[a..z0..9]*(.*)" % SQ_SESSION_KEY
-            clean_url = re.sub(regex_no_session_key, "\\1", url)
-            return redirect(clean_url)
-
-        if session_token == get_token:
-            return view(request, *args, **kwargs)
-
-        raise Exception('SQ')
-
-    return _wrapped
diff --git a/secretquestions/forms.py b/secretquestions/forms.py
deleted file mode 100644 (file)
index 1bcf3e8..0000000
+++ /dev/null
@@ -1,87 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from django.conf import settings
-from django import forms
-from django.forms.models import modelformset_factory, ModelForm
-from django.utils.translation import ugettext as _
-
-from django.contrib.auth.models import User
-
-from .models import Answer, crypt_answer, check_answer
-
-
-MAX_SECRET_QUESTIONS = getattr(settings, 'MAX_SECRET_QUESTIONS', 3)
-
-
-class AnswerForm(ModelForm):
-
-    class Meta:
-        model = Answer
-
-    def __init__(self, *args, **kwargs):
-        if 'instance' in kwargs:
-            kwargs['instance'].secret = ""
-        super(AnswerForm, self).__init__(*args, **kwargs)
-
-    def clean_secret(self):
-        data = self.cleaned_data['secret']
-        return crypt_answer(data)
-
-
-_FreeAnswerFormSet = modelformset_factory(Answer,
-                                          form=AnswerForm,
-                                          fields=("question", "secret"),
-                                          extra=MAX_SECRET_QUESTIONS,
-                                          max_num=MAX_SECRET_QUESTIONS,
-                                          can_delete=False)
-
-
-class AnswerFormSet(_FreeAnswerFormSet):
-
-    def __init__(self, *args, **kwargs):
-        self.user = kwargs.pop('user')
-        super(AnswerFormSet, self).__init__(*args, **kwargs)
-
-    def save_all(self):
-        instances = self.save(commit=False)
-        for instance in instances:
-            instance.user = self.user
-            instance.save()
-
-    def clean(self):
-        questions = []
-        for i in range(0, self.total_form_count()):
-            form = self.forms[i]
-            try:
-                question = form.cleaned_data.get('question')
-            except:
-                question = None
-            if question is None:
-                error_msg = _("All questions have to be selected.")
-                raise forms.ValidationError(error_msg)
-            if question in questions:
-                error_msg = _("Each question has to be different.")
-                raise forms.ValidationError(error_msg)
-            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."))
diff --git a/secretquestions/migrations/0001_initial.py b/secretquestions/migrations/0001_initial.py
deleted file mode 100644 (file)
index 02ce945..0000000
+++ /dev/null
@@ -1,87 +0,0 @@
-# -*- coding: utf-8 -*-
-import datetime
-from south.db import db
-from south.v2 import SchemaMigration
-from django.db import models
-
-
-class Migration(SchemaMigration):
-
-    def forwards(self, orm):
-        # Adding model 'Question'
-        db.create_table('secretquestions_question', (
-            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
-            ('text', self.gf('django.db.models.fields.CharField')(max_length=255)),
-        ))
-        db.send_create_signal('secretquestions', ['Question'])
-
-        # Adding model 'Answer'
-        db.create_table('secretquestions_answer', (
-            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
-            ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
-            ('question', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['secretquestions.Question'])),
-            ('secret', self.gf('django.db.models.fields.CharField')(max_length=255)),
-        ))
-        db.send_create_signal('secretquestions', ['Answer'])
-
-
-    def backwards(self, orm):
-        # Deleting model 'Question'
-        db.delete_table('secretquestions_question')
-
-        # Deleting model 'Answer'
-        db.delete_table('secretquestions_answer')
-
-
-    models = {
-        'auth.group': {
-            'Meta': {'object_name': 'Group'},
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
-            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
-            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
-        },
-        'auth.permission': {
-            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
-            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
-            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
-        },
-        'auth.user': {
-            'Meta': {'object_name': 'User'},
-            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
-            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
-            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
-            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
-            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
-            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
-            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
-            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
-            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
-            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
-            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
-            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
-        },
-        'contenttypes.contenttype': {
-            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
-            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
-            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
-        },
-        'secretquestions.answer': {
-            'Meta': {'object_name': 'Answer'},
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
-            'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['secretquestions.Question']"}),
-            'secret': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
-            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
-        },
-        'secretquestions.question': {
-            'Meta': {'object_name': 'Question'},
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
-            'text': ('django.db.models.fields.CharField', [], {'max_length': '255'})
-        }
-    }
-
-    complete_apps = ['secretquestions']
\ No newline at end of file
diff --git a/secretquestions/migrations/__init__.py b/secretquestions/migrations/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/secretquestions/models.py b/secretquestions/models.py
deleted file mode 100644 (file)
index 61a9353..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from django.db import models
-
-from django.contrib.auth.models import get_hexdigest, check_password
-
-
-def crypt_answer(raw):
-    import random
-    algo = 'sha1'
-    salt = get_hexdigest(algo, str(random.random()), str(random.random()))[:5]
-    hsh = get_hexdigest(algo, salt, 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)
-
-    def __unicode__(self):
-        return self.text
-
-
-class Answer(models.Model):
-    user = models.ForeignKey('auth.User', related_name="secret_answers")
-    question = models.ForeignKey('secretquestions.Question')
-    secret = models.CharField(max_length=255)
-
-    class Meta:
-        unique_together = ('question', 'user')
diff --git a/secretquestions/templates/base.html b/secretquestions/templates/base.html
deleted file mode 100644 (file)
index e5ba34e..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-<html>
-    <body>
-    {% block content %}
-    {% endblock content %}
-    </body>
-</html>
diff --git a/secretquestions/templates/secretquestions/setup_form.html b/secretquestions/templates/secretquestions/setup_form.html
deleted file mode 100644 (file)
index c76e9d6..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-{% extends "base.html" %}
-{% load i18n %}
-
-{% block content %}
-
-<p>
-{% blocktrans %}
-Select several questions and register your secret answers.
-These one will be asked to you for some critical features which required more
-security to ensure that is you.
-{% endblocktrans %}
-</p>
-
-{{ formset.non_form_errors.as_ul }}
-
-<form action="" method="post">{% csrf_token %}
-    <table>
-        {{ formset }}
-    </table>
-    <input type="submit" value="{% trans "Save all" %}" />
-</form>
-
-{% endblock content %}
diff --git a/secretquestions/templates/secretquestions/step.html b/secretquestions/templates/secretquestions/step.html
deleted file mode 100644 (file)
index 69a8dd0..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-{% 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
deleted file mode 100644 (file)
index 617f330..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-from configuration import ConfigurationTest
-from use import UseTest
diff --git a/secretquestions/tests/common.py b/secretquestions/tests/common.py
deleted file mode 100644 (file)
index b2d644f..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from django.test import TestCase
-from django.test.client import Client
-
-from django.contrib.auth.models import User
-
-from secretquestions.models import Question, Answer, crypt_answer
-
-
-class SecretQuestionTest(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.question1.save()
-        self.question2 = Question.objects.create(text="question2")
-        self.question2.save()
-
-    def create_answers(self):
-        self.answer1 = Answer.objects.create(question=self.question1,
-                                             secret=crypt_answer('one'),
-                                             user=self.user)
-        self.answer1.save()
-        self.answer2 = Answer.objects.create(question=self.question2,
-                                             secret=crypt_answer('two'),
-                                             user=self.user)
-        self.answer2.save()
diff --git a/secretquestions/tests/configuration.py b/secretquestions/tests/configuration.py
deleted file mode 100644 (file)
index 03ed23d..0000000
+++ /dev/null
@@ -1,89 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from django.core.urlresolvers import reverse
-from django.conf import settings
-
-from secretquestions.tests.common import SecretQuestionTest
-from secretquestions.models import Answer
-
-
-class ConfigurationTest(SecretQuestionTest):
-    """
-    TestCase for setting questions/answers
-    """
-
-    def test_access_setup_questions_for_anonymous(self):
-        """
-        Check if you try to access setup page as anonymous, you're redirected
-        to login page.
-        """
-        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):
-        """
-        Check if setup page is accessible from authenticated people
-        """
-        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):
-        """
-        Check if the answer is really stored and crypted
-        """
-        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):
-        """
-        Check if the answer is not empty
-        """
-        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):
-        """
-        Check if you have ever set answer, the form does not prepopulate
-        this one.
-        """
-        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
deleted file mode 100644 (file)
index 1deaf72..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-# -*- 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
deleted file mode 100644 (file)
index b0dd12e..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-from django.conf.urls.defaults import patterns, include, url
-
-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')),
-                       url(r'^test_public/',
-                           'secretquestions.tests.views.public',
-                           name='sq_test_public'),
-                       url(r'^test_private/',
-                           'secretquestions.tests.views.private',
-                           name='sq_test_private'),)
diff --git a/secretquestions/tests/use.py b/secretquestions/tests/use.py
deleted file mode 100644 (file)
index 955dcf4..0000000
+++ /dev/null
@@ -1,178 +0,0 @@
-# -*- coding: utf-8 -*-
-
-import re
-
-from django.core.urlresolvers import reverse
-
-from secretquestions.tests.common import SecretQuestionTest
-from secretquestions.conf import SQ_SESSION_KEY
-
-
-class UseTest(SecretQuestionTest):
-    """
-    TestCase for accessing guarded pages.
-    """
-
-    def test_access_public_for_anonymous(self):
-        """
-        Check if you try to access public page as anonymous
-        """
-        url = reverse('sq_test_public')
-        response = self.client.get(url)
-        self.assertEqual(response.status_code, 200)
-        self.assertTrue('OK' in response.content)
-
-    def test_access_public_for_authenticated(self):
-        """
-        Check if you try to access public page as authenticated
-        """
-        self.assertEqual(self.client.login(username=self.username,
-                                           password=self.password), True)
-        url = reverse('sq_test_public')
-        response = self.client.get(url)
-        self.assertEqual(response.status_code, 200)
-        self.assertTrue('OK' in response.content)
-
-    def test_access_private_for_anonymous(self):
-        """
-        Check if you try to access private page as anonymous,
-        you are asking for username
-        """
-        url = reverse('sq_test_private')
-        response = self.client.get(url)
-        self.assertEqual(response.status_code, 200)
-        self.assertFalse('OK' in response.content)
-        self.assertTrue('username' in response.content)
-
-    def test_access_private_for_authenticated(self):
-        """
-        Check if you try to access private page as authenticated,
-        you are asking for first question
-        """
-        self.create_answers()
-        self.assertEqual(self.client.login(username=self.username,
-                                           password=self.password), True)
-        url = reverse('sq_test_private')
-        response = self.client.get(url)
-        self.assertEqual(response.status_code, 200)
-        self.assertFalse('OK' in response.content)
-        self.assertFalse('username' in response.content)
-
-    def test_access_private_without_question(self):
-        """
-        Check if you try to access private page as authenticated,
-        you don't have set any secret questions yet
-        you are redirected to the page
-        """
-        self.assertEqual(self.client.login(username=self.username,
-                                           password=self.password), True)
-        url = reverse('sq_test_private')
-        response = self.client.get(url)
-        self.assertEqual(response.status_code, 302)
-        self.assertFalse('OK' in response.content)
-        self.assertFalse('username' in response.content)
-        self.assertTrue(url in response['location'])
-        self.assertTrue(SQ_SESSION_KEY in response['location'])
-
-    def test_username_form(self):
-        """
-        Test form submission
-        """
-        url = reverse('sq_test_private')
-        response = self.client.get(url)
-        self.assertEqual(response.status_code, 200)
-        data = {'0-username': '', }
-        response = self.client.post(url, data)
-        self.assertTrue('This field is required' in response.content)
-        data = {'0-username': 'fake', }
-        response = self.client.post(url, data)
-        self.assertTrue('Username not found' in response.content)
-        data = {'0-username': self.username, }
-        response = self.client.post(url, data)
-        self.assertEqual(response.status_code, 302)
-        self.assertTrue(url in response['location'])
-        self.assertTrue(SQ_SESSION_KEY in response['location'])
-
-    def _get_hashs(self, response):
-        """
-        Parse response to prepare POST according previous hash
-        """
-        regex_hash = 'name="(hash_[0-9])+" value="([a-z0-9]+)"'
-        found = re.findall(regex_hash, response.content)
-        hashs = {}
-        for k, v in found:
-            hashs.update({k: v})
-        return hashs
-
-    def test_question_form(self):
-        """
-        Test form submission
-        """
-        self.create_answers()
-        url = reverse('sq_test_private')
-        response = self.client.get(url)
-        self.assertEqual(response.status_code, 200)
-        data = {'wizard_step': 0,
-                '0-username': self.username, }
-        response = self.client.post(url, data)
-        self.assertEqual(response.status_code, 200)
-        self.assertTrue(self.question1.text in response.content)
-
-        # wrong response
-        data = {'wizard_step': 1,
-                '0-username': self.username,
-                '1-raw_answer': 'wrong answer', }
-        data.update(self._get_hashs(response))
-        response = self.client.post(url, data)
-
-        self.assertEqual(response.status_code, 200)
-        self.assertFalse('2-raw_answer' in response.content)
-
-        # good response
-        data = {'wizard_step': 1,
-                '0-username': self.username,
-                '1-raw_answer': 'one', }
-        data.update(self._get_hashs(response))
-        response = self.client.post(url, data)
-        self.assertEqual(response.status_code, 200)
-        self.assertTrue('2-raw_answer' in response.content)
-
-        # good response
-        data = {'wizard_step': 2,
-                '0-username': self.username,
-                '1-raw_answer': 'one',
-                '2-raw_answer': 'two', }
-        data.update(self._get_hashs(response))
-        response = self.client.post(url, data)
-        self.assertEqual(response.status_code, 302)
-        self.assertTrue(url in response['location'])
-        self.assertTrue(SQ_SESSION_KEY in response['location'])
-
-        response = self.client.get(response['location'])
-        self.assertTrue('OK' in response.content)
-
-    def test_inconsistent_data(self):
-        """
-        Test inconsistent data between steps
-        wizard toolkit redirect on step with corrupted data
-        """
-        self.create_answers()
-        url = reverse('sq_test_private')
-        response = self.client.get(url)
-        data = {'wizard_step': 0,
-                '0-username': self.username, }
-        response = self.client.post(url, data)
-        self.assertTrue('1-raw_answer' in response.content)
-        data = {'wizard_step': 1,
-                '0-username': self.username,
-                '1-raw_answer': 'one', }
-        data.update(self._get_hashs(response))
-        response = self.client.post(url, data)
-        self.assertTrue('2-raw_answer' in response.content)
-        data = {'wizard_step': 2,
-                '0-username': self.username,
-                '1-raw_answer': 'xxx',
-                '2-raw_answer': 'two', }
-        data.update(self._get_hashs(response))
-        response = self.client.post(url, data)
-        self.assertTrue('1-raw_answer' in response.content)
diff --git a/secretquestions/tests/views.py b/secretquestions/tests/views.py
deleted file mode 100644 (file)
index 4d1d191..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from django.http import HttpResponse
-
-from secretquestions.decorators import secret_questions_required
-
-
-def public(request):
-    return HttpResponse("OK")
-
-
-@secret_questions_required
-def private(request):
-    return HttpResponse("OK")
diff --git a/secretquestions/urls.py b/secretquestions/urls.py
deleted file mode 100644 (file)
index d2198f9..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from django.conf.urls.defaults import patterns, url
-
-urlpatterns = patterns('',
-                       url(r'questions/setup$',
-                           'secretquestions.views.setup_form',
-                           name="sq_setup"), )
diff --git a/secretquestions/views.py b/secretquestions/views.py
deleted file mode 100644 (file)
index e638095..0000000
+++ /dev/null
@@ -1,101 +0,0 @@
-# -*- 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 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):
-    if request.method == 'POST':
-        formset = AnswerFormSet(request.POST, user=request.user)
-        if formset.is_valid():
-            formset.save_all()
-            messages.info(request,
-                          _("Your secret answers were successfully saved."))
-            return redirect(settings.LOGIN_REDIRECT_URL)
-    else:
-        formset = AnswerFormSet(user=request.user)
-    return render_to_response("secretquestions/setup_form.html", {
-        "formset": formset,
-        },
-        context_instance=RequestContext(request))
-
-
-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():
-            self.user = request.user
-            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):
-        # double check validation, it's already done
-        # by wizard and redirect on the step with corrupted data
-        for form in form_list:
-            if not form.is_valid():
-                raise Exception('SQ corrupted data')  # pragma: no cover
-
-        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)
-
-    def __call__(self, request, *args, **kwargs):
-        if len(self.form_list) == 0:
-            return self.done(request, self.form_list)
-        return super(SecretQuestionWizard, self).\
-            __call__(request, *args, **kwargs)
index 5061f71..6e878f8 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -2,8 +2,9 @@ from setuptools import setup, find_packages
 import sys, os
 
 version = '0.0'
+name = 'auf.django.secretquestions'
 
-setup(name='django-secretquestions',
+setup(name=name,
       version=version,
       description="Provides secret questions toolkit",
       long_description="""\
@@ -12,9 +13,10 @@ setup(name='django-secretquestions',
       keywords='django secretquestions authentication security',
       author='Olivier Larchev\xc3\xaaque',
       author_email='olivier.larcheveque@auf.org',
-      url='',
-      license='',
+      url='http://pypi.auf.org/%s' % name,
+      license='GPL',
       packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
+      namespace_packages = ['auf'],
       include_package_data=True,
       zip_safe=False,
       install_requires=[
diff --git a/tox.ini b/tox.ini
index ba79398..95f0919 100644 (file)
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist = django1.3, django1.4
+envlist = django1.3
 
 [testenv]
 deps =
@@ -9,16 +9,12 @@ deps =
 
 commands =
     coverage erase
-    coverage run --source="{envsitepackagesdir}/secretquestions/" {envdir}/bin/django-admin.py test secretquestions --settings=secretquestions.tests.settings
-    pep8 -r  --statistics  --count {envsitepackagesdir}/secretquestions/ --exclude={envsitepackagesdir}/secretquestions/migrations/*
+    coverage run --source="{envsitepackagesdir}/auf/django/secretquestions/" {envdir}/bin/django-admin.py test secretquestions --settings=auf.django.secretquestions.tests.settings
+    pep8 -r  --statistics  --count {envsitepackagesdir}/auf/django/secretquestions/ --exclude={envsitepackagesdir}/auf/django/secretquestions/migrations/*
     coverage report
+    coverage html
 
 [testenv:django1.3]
 deps =
     {[testenv]deps}
     django==1.3
-
-[testenv:django1.4]
-deps =
-    {[testenv]deps}
-    django==1.4