initial commit
authorRadek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>
Tue, 20 Sep 2011 15:12:24 +0000 (17:12 +0200)
committerRadek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>
Tue, 20 Sep 2011 15:13:56 +0000 (17:13 +0200)
28 files changed:
.gitignore [new file with mode: 0644]
README.rst [new file with mode: 0644]
apps/quiz/__init__.py [new file with mode: 0644]
apps/quiz/admin.py [new file with mode: 0644]
apps/quiz/forms.py [new file with mode: 0644]
apps/quiz/locale/pl/LC_MESSAGES/django.mo [new file with mode: 0644]
apps/quiz/locale/pl/LC_MESSAGES/django.po [new file with mode: 0644]
apps/quiz/migrations/0001_initial.py [new file with mode: 0644]
apps/quiz/migrations/__init__.py [new file with mode: 0644]
apps/quiz/models.py [new file with mode: 0644]
apps/quiz/templates/quiz/question_detail.html [new file with mode: 0755]
apps/quiz/templates/quiz/result_detail.html [new file with mode: 0755]
apps/quiz/urls.py [new file with mode: 0644]
apps/quiz/views.py [new file with mode: 0644]
fabfile.py [new file with mode: 0644]
koedquiz.vhost.template [new file with mode: 0644]
koedquiz.wsgi.template [new file with mode: 0644]
koedquiz/__init__.py [new file with mode: 0644]
koedquiz/localsettings.py.template [new file with mode: 0644]
koedquiz/manage.py [new file with mode: 0755]
koedquiz/settings.py [new file with mode: 0644]
koedquiz/templates/base.html [new file with mode: 0644]
koedquiz/templates/home.html [new file with mode: 0644]
koedquiz/urls.py [new file with mode: 0644]
koedquiz/views.py [new file with mode: 0644]
lib/git-archive-all.sh [new file with mode: 0644]
requirements-dev.txt [new file with mode: 0644]
requirements.txt [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..d1146e9
--- /dev/null
@@ -0,0 +1,23 @@
+localsettings.py
+*.db
+*.db-journal
+*~
+
+# Python garbage
+*.pyc
+.coverage
+pip-log.txt
+nosetests.xml
+
+# Mac OS X garbage
+.DS_Store
+
+# Windows garbage
+thumbs.db
+
+# Eclipse
+.project
+.settings
+.pydevproject
+.tmp_*
+
diff --git a/README.rst b/README.rst
new file mode 100644 (file)
index 0000000..62a06e2
--- /dev/null
@@ -0,0 +1,5 @@
+=========
+KOED Quiz
+=========
+
+This project will be used for various openness related tests and quizzes.
diff --git a/apps/quiz/__init__.py b/apps/quiz/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/apps/quiz/admin.py b/apps/quiz/admin.py
new file mode 100644 (file)
index 0000000..849f918
--- /dev/null
@@ -0,0 +1,18 @@
+from django.contrib import admin
+
+from quiz.models import Question, Result, Answer
+
+
+class AnswerInline(admin.TabularInline):
+    model = Answer
+    fk_name = 'question'
+    extra = 2
+
+
+class QuestionAdmin(admin.ModelAdmin):
+    inlines = [AnswerInline]
+    list_display = ['title', 'quiz']
+
+
+admin.site.register(Question, QuestionAdmin)
+admin.site.register(Result)
diff --git a/apps/quiz/forms.py b/apps/quiz/forms.py
new file mode 100644 (file)
index 0000000..7831806
--- /dev/null
@@ -0,0 +1,14 @@
+from django import forms
+from django.forms.widgets import RadioSelect
+from django.utils.translation import ugettext_lazy as _
+from quiz.models import Answer
+
+class QuestionForm(forms.Form):
+    answer = forms.ModelChoiceField(widget=RadioSelect,
+        queryset=Answer.objects.all(), empty_label=None,
+        error_messages={'required': _('Please select an option')})
+
+    def __init__(self, instance, *args, **kwargs):
+        r = super(QuestionForm, self).__init__(*args, **kwargs)
+        self.fields['answer'].queryset = instance.answer_set.all()
+        return r
diff --git a/apps/quiz/locale/pl/LC_MESSAGES/django.mo b/apps/quiz/locale/pl/LC_MESSAGES/django.mo
new file mode 100644 (file)
index 0000000..7b3fdb2
Binary files /dev/null and b/apps/quiz/locale/pl/LC_MESSAGES/django.mo differ
diff --git a/apps/quiz/locale/pl/LC_MESSAGES/django.po b/apps/quiz/locale/pl/LC_MESSAGES/django.po
new file mode 100644 (file)
index 0000000..62433dd
--- /dev/null
@@ -0,0 +1,71 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Quiz 0.1\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2011-09-20 17:10+0200\n"
+"PO-Revision-Date: 2011-09-20 17:10+0100\n"
+"Last-Translator: Radek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: pl\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
+"|| n%100>=20) ? 1 : 2);\n"
+
+#: forms.py:9
+msgid "Please select an option"
+msgstr "Proszę wybrać jedną z odpowiedzi"
+
+#: models.py:11
+msgid "quiz"
+msgstr "quiz"
+
+#: models.py:12
+msgid "quizzes"
+msgstr "quizy"
+
+#: models.py:40
+msgid "result"
+msgstr "rezultat"
+
+#: models.py:41
+msgid "results"
+msgstr "rezultaty"
+
+#: models.py:60
+msgid "question"
+msgstr "pytanie"
+
+#: models.py:61
+msgid "questions"
+msgstr "pytania"
+
+#: models.py:90
+msgid "answer"
+msgstr "odpowiedź"
+
+#: models.py:91
+msgid "answers"
+msgstr "odpowiedzi"
+
+#: templates/quiz/question_detail.html:26 templates/quiz/result_detail.html:20
+msgid "Back to last question"
+msgstr "Wróć do poprzedniego pytania"
+
+#: templates/quiz/question_detail.html:31 templates/quiz/result_detail.html:24
+msgid "Back to my test"
+msgstr "Wróć do testu"
+
+#: templates/quiz/question_detail.html:33 templates/quiz/result_detail.html:26
+msgid "Start from the beginning"
+msgstr "Zacznij od początku"
+
+#: templates/quiz/result_detail.html:32
+msgid "Start again"
+msgstr "Jeszcze raz od początku"
diff --git a/apps/quiz/migrations/0001_initial.py b/apps/quiz/migrations/0001_initial.py
new file mode 100644 (file)
index 0000000..df82327
--- /dev/null
@@ -0,0 +1,108 @@
+# encoding: 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 'Result'
+        db.create_table('quiz_result', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('quiz', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['sites.Site'])),
+            ('slug', self.gf('django.db.models.fields.SlugField')(max_length=50, db_index=True)),
+            ('title', self.gf('django.db.models.fields.CharField')(max_length=255)),
+            ('text', self.gf('django.db.models.fields.TextField')()),
+        ))
+        db.send_create_signal('quiz', ['Result'])
+
+        # Adding model 'Question'
+        db.create_table('quiz_question', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('quiz', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['sites.Site'])),
+            ('slug', self.gf('django.db.models.fields.SlugField')(max_length=50, db_index=True)),
+            ('ordering', self.gf('django.db.models.fields.SmallIntegerField')()),
+            ('title', self.gf('django.db.models.fields.CharField')(max_length=255)),
+            ('text', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
+            ('description', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
+        ))
+        db.send_create_signal('quiz', ['Question'])
+
+        # Adding unique constraint on 'Question', fields ['quiz', 'slug']
+        db.create_unique('quiz_question', ['quiz_id', 'slug'])
+
+        # Adding unique constraint on 'Question', fields ['quiz', 'ordering']
+        db.create_unique('quiz_question', ['quiz_id', 'ordering'])
+
+        # Adding model 'Answer'
+        db.create_table('quiz_answer', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('title', self.gf('django.db.models.fields.CharField')(max_length=255)),
+            ('question', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['quiz.Question'])),
+            ('go_to', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='go_tos', null=True, to=orm['quiz.Question'])),
+            ('result', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['quiz.Result'], null=True, blank=True)),
+            ('ordering', self.gf('django.db.models.fields.SmallIntegerField')()),
+        ))
+        db.send_create_signal('quiz', ['Answer'])
+
+
+    def backwards(self, orm):
+        
+        # Removing unique constraint on 'Question', fields ['quiz', 'ordering']
+        db.delete_unique('quiz_question', ['quiz_id', 'ordering'])
+
+        # Removing unique constraint on 'Question', fields ['quiz', 'slug']
+        db.delete_unique('quiz_question', ['quiz_id', 'slug'])
+
+        # Deleting model 'Result'
+        db.delete_table('quiz_result')
+
+        # Deleting model 'Question'
+        db.delete_table('quiz_question')
+
+        # Deleting model 'Answer'
+        db.delete_table('quiz_answer')
+
+
+    models = {
+        'quiz.answer': {
+            'Meta': {'ordering': "['question', 'ordering']", 'object_name': 'Answer'},
+            'go_to': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'go_tos'", 'null': 'True', 'to': "orm['quiz.Question']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ordering': ('django.db.models.fields.SmallIntegerField', [], {}),
+            'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['quiz.Question']"}),
+            'result': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['quiz.Result']", 'null': 'True', 'blank': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'quiz.question': {
+            'Meta': {'ordering': "['quiz', 'ordering']", 'unique_together': "[['quiz', 'slug'], ['quiz', 'ordering']]", 'object_name': 'Question'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ordering': ('django.db.models.fields.SmallIntegerField', [], {}),
+            'quiz': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sites.Site']"}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'}),
+            'text': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'quiz.quiz': {
+            'Meta': {'ordering': "('domain',)", 'object_name': 'Quiz', 'db_table': "'django_site'", '_ormbases': ['sites.Site'], 'proxy': 'True'}
+        },
+        'quiz.result': {
+            'Meta': {'object_name': 'Result'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'quiz': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sites.Site']"}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'}),
+            'text': ('django.db.models.fields.TextField', [], {}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'sites.site': {
+            'Meta': {'ordering': "('domain',)", 'object_name': 'Site', 'db_table': "'django_site'"},
+            'domain': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        }
+    }
+
+    complete_apps = ['quiz']
diff --git a/apps/quiz/migrations/__init__.py b/apps/quiz/migrations/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/apps/quiz/models.py b/apps/quiz/models.py
new file mode 100644 (file)
index 0000000..92955fa
--- /dev/null
@@ -0,0 +1,105 @@
+from django.db import models
+from django.contrib.sites.models import Site
+from django.utils.translation import ugettext_lazy as _
+
+from django.conf import settings
+
+
+class Quiz(Site):
+    class Meta:
+        proxy=True
+        verbose_name = _('quiz')
+        verbose_name_plural = _('quizzes')
+
+    @classmethod
+    def current(cls):
+        return cls.objects.get(id=settings.SITE_ID)
+
+    def start(self):
+        return self.question_set.all()[0]
+
+    @models.permalink
+    def get_absolute_url(self):
+        return ('quiz', )
+
+    def where_to(self):
+        try:
+            return Result.objects.get(quiz=self).get_absolute_url()
+        except Result.DoesNotExist:
+            # just go to the beginning
+            return self.get_absolute_url()
+
+
+class Result(models.Model):
+    quiz = models.ForeignKey(Quiz)
+    slug = models.SlugField(db_index=True)
+    title = models.CharField(max_length=255)
+    text = models.TextField()
+
+    class Meta:
+        verbose_name = _('result')
+        verbose_name_plural = _('results')
+
+    def __unicode__(self):
+        return self.title
+
+    @models.permalink
+    def get_absolute_url(self):
+        return ('quiz_result', [self.slug])
+
+
+class Question(models.Model):
+    quiz = models.ForeignKey(Quiz)
+    slug = models.SlugField(db_index=True)
+    ordering = models.SmallIntegerField()
+    title = models.CharField(max_length=255)
+    text = models.TextField(null=True, blank=True)
+    description = models.TextField(null=True, blank=True)
+
+    class Meta:
+        verbose_name = _('question')
+        verbose_name_plural = _('questions')
+        ordering = ['quiz', 'ordering']
+        unique_together = [['quiz', 'slug'], ['quiz', 'ordering']]
+
+    def __unicode__(self):
+        return self.title
+
+    @models.permalink
+    def get_absolute_url(self):
+        return ('quiz', [self.slug])
+
+    def where_to(self):
+        later = self.quiz.question_set.filter(ordering__gt=self.ordering)
+        if later.exists():
+            return later[0].get_absolute_url()
+        else:
+            return self.quiz.where_to()
+
+
+
+class Answer(models.Model):
+    title = models.CharField(max_length=255)
+    question = models.ForeignKey(Question)
+    go_to = models.ForeignKey(Question, null=True, blank=True,
+            related_name='go_tos')
+    result = models.ForeignKey(Result, null=True, blank=True)
+    ordering = models.SmallIntegerField()
+
+    class Meta:
+        verbose_name = _('answer')
+        verbose_name_plural = _('answers')
+        ordering = ['question', 'ordering']
+
+    def __unicode__(self):
+        return self.title
+
+    def where_to(self):
+        # follow explicit redirects
+        if self.result:
+            return self.result.get_absolute_url()
+        elif self.go_to:
+            return self.go_to.get_absolute_url()
+
+        # or just get the next question
+        return self.question.where_to()
diff --git a/apps/quiz/templates/quiz/question_detail.html b/apps/quiz/templates/quiz/question_detail.html
new file mode 100755 (executable)
index 0000000..b739a98
--- /dev/null
@@ -0,0 +1,46 @@
+{% extends "base.html" %}
+{% load i18n %}
+{% load url from future %}
+
+{% block "body" %}
+
+
+<h1>{{ question.title }}</h1>
+<div>
+{{ question.text }}
+</div>
+
+<div>
+
+
+
+<form method='post'>
+{% csrf_token %}
+{{ form.answer.errors }}
+{{ form.answer }}
+
+{% if valid %}
+    <input type='submit' />
+
+    {% if previous_url %}
+        <a href='{{ previous_url }}'>{% trans "Back to last question" %}</a>
+    {% endif %}
+
+{% else %}
+    {% if valid_url %}
+        <a href='{{ valid_url }}'>{% trans "Back to my test" %}</a>
+    {% else %}
+        <a href='{% url "quiz" %}'>{% trans "Start from the beginning" %}</a>
+    {% endif %}
+{% endif %}
+
+</form>
+</div>
+
+
+<div>
+{{ question.description }}
+</div>
+
+
+{% endblock %}
diff --git a/apps/quiz/templates/quiz/result_detail.html b/apps/quiz/templates/quiz/result_detail.html
new file mode 100755 (executable)
index 0000000..89b875c
--- /dev/null
@@ -0,0 +1,34 @@
+{% extends "base.html" %}
+{% load i18n %}
+{% load url from future %}
+
+
+{% block "body" %}
+
+<h1>{{ result.title }}</h1>
+
+
+
+<div>
+{{ result.text }}
+</div>
+
+
+
+{% if valid %}
+    {% if previous_url %}
+        <a href='{{ previous_url }}'>{% trans "Back to last question" %}</a>
+    {% endif %}
+{% else %}
+    {% if valid_url %}
+        <a href='{{ valid_url }}'>{% trans "Back to my test" %}</a>
+    {% else %}
+        <a href='{% url "quiz" %}'>{% trans "Start from the beginning" %}</a>
+    {% endif %}
+{% endif %}
+
+
+
+<a href='{% url "quiz" %}'>{% trans "Start again" %}</a>
+
+{% endblock %}
diff --git a/apps/quiz/urls.py b/apps/quiz/urls.py
new file mode 100644 (file)
index 0000000..ae5c683
--- /dev/null
@@ -0,0 +1,8 @@
+from django.conf.urls.defaults import *
+
+
+urlpatterns = patterns('quiz.views',
+    url(r'^$', 'question', name='quiz'),
+    url(r'^q/(?P<slug>[-a-z0-9]*)/$', 'question', name='quiz'),
+    url(r'^r/(?P<slug>[-a-z0-9]*)/$', 'result', name='quiz_result'),
+)
diff --git a/apps/quiz/views.py b/apps/quiz/views.py
new file mode 100644 (file)
index 0000000..7160759
--- /dev/null
@@ -0,0 +1,52 @@
+from django.shortcuts import get_object_or_404, render, redirect
+
+from quiz.forms import QuestionForm
+from quiz.models import Quiz
+
+
+def question(request, slug=None):
+    if slug is None:
+        question = Quiz.current().start()
+        request.session['ticket'] = [request.path]
+    else:
+        question = get_object_or_404(Quiz.current().question_set, slug=slug)
+
+    ticket = request.session.get('ticket')
+    valid = request.path in ticket
+    print ticket, valid
+    if valid:
+        cur_index = ticket.index(request.path)
+        if cur_index:
+            previous_url = ticket[cur_index - 1]
+    else:
+        valid_url = ticket[-1]
+
+    if request.method == 'POST' and valid:
+        form = QuestionForm(question, request.POST)
+        if form.is_valid():
+
+            answer = form.cleaned_data['answer']
+            where_to = answer.where_to()
+
+            del ticket[cur_index + 1:]
+            try:
+                del ticket[ticket.index(where_to) + 1:]
+            except ValueError:
+                ticket.append(where_to)
+
+            request.session['ticket'] = ticket
+
+            return redirect(where_to)
+    else:
+        form = QuestionForm(question)
+
+    return render(request, "quiz/question_detail.html", locals())
+
+
+def result(request, slug=None):
+    ticket = request.session['ticket']
+    valid = request.path in ticket
+
+    result = get_object_or_404(Quiz.current().result_set, slug=slug)
+    return render(request, "quiz/result_detail.html", locals())
+
diff --git a/fabfile.py b/fabfile.py
new file mode 100644 (file)
index 0000000..c4965de
--- /dev/null
@@ -0,0 +1,211 @@
+from __future__ import with_statement # needed for python 2.5
+from fabric.api import *
+from fabric.contrib import files
+
+import os
+
+
+# ==========
+# = Config =
+# ==========
+# Globals
+env.project_name = 'koedquiz'
+env.use_south = True
+
+# Servers
+def localhost():
+    """SSH to localhost (for debugging).
+
+    This will deploy to `test-deployment` in the project dir.
+
+    """
+    import os.path
+    from getpass import getuser
+
+    env.hosts = ['localhost']
+    env.user = getuser()
+    env.path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'test-deployment')
+    env.virtualenv = '/usr/bin/virtualenv'
+    # This goes to VHost configuration
+    env.server_name = 'koedquiz.example.com'
+    env.server_admin = 'koedquiz <koedquiz@koedquiz.example.com>'
+    # /var/log/apache2/* logs
+    env.access_log = 'koedquiz.log'
+    env.error_log = 'koedquiz-errors.log'
+
+
+# add additional servers here
+
+
+servers = [localhost]
+
+# =========
+# = Tasks =
+# =========
+def test():
+    "Run the test suite and bail out if it fails"
+    require('hosts', 'path', provided_by=servers)
+    require('python', provided_by=[find_python])
+    result = run('cd %(path)s/%(project_name)s; %(python)s manage.py test' % env)
+
+def setup():
+    """
+    Setup a fresh virtualenv as well as a few useful directories, then run
+    a full deployment. virtualenv with pip should be already installed.
+    """
+    require('hosts', 'path', 'virtualenv', provided_by=servers)
+
+    run('mkdir -p %(path)s; cd %(path)s; %(virtualenv)s ve;' % env, pty=True)
+    run('cd %(path)s; mkdir releases; mkdir packages;' % env, pty=True)
+    run('cd %(path)s/releases; ln -s . current; ln -s . previous' % env, pty=True)
+    upload_default_localsettings()
+    deploy()
+
+def deploy():
+    """
+    Deploy the latest version of the site to the servers,
+    install any required third party modules,
+    install the virtual host and then restart the webserver
+    """
+
+    import time
+    env.release = time.strftime('%Y-%m-%dT%H%M')
+
+    upload_tar_from_git()
+    find_python()
+    upload_wsgi_script()
+    upload_vhost_sample()
+    install_requirements()
+    copy_localsettings()
+    symlink_current_release()
+    collectstatic()
+    migrate()
+    restart_webserver()
+
+def deploy_version(version):
+    "Specify a specific version to be made live"
+    require('hosts', 'path', provided_by=servers)
+    env.version = version
+    with cd(env.path):
+        run('rm releases/previous; mv releases/current releases/previous;', pty=True)
+        run('ln -s %(version)s releases/current' % env, pty=True)
+    restart_webserver()
+
+def rollback():
+    """
+    Limited rollback capability. Simple loads the previously current
+    version of the code. Rolling back again will swap between the two.
+    """
+    require('hosts', 'path', provided_by=servers)
+    with cd(env.path):
+        run('mv releases/current releases/_previous;', pty=True)
+        run('mv releases/previous releases/current;', pty=True)
+        run('mv releases/_previous releases/previous;', pty=True)
+    restart_webserver()
+
+
+# =====================================================================
+# = Helpers. These are called by other functions rather than directly =
+# =====================================================================
+def upload_tar_from_git():
+    "Create an archive from the current Git branch and upload it"
+    print '>>> upload tar from git'
+    require('path', provided_by=servers)
+    require('release', provided_by=[deploy])
+    local('/bin/bash lib/git-archive-all.sh --format tar %(release)s.tar' % env)
+    local('gzip %(release)s.tar' % env)
+    run('mkdir -p %(path)s/releases/%(release)s' % env, pty=True)
+    run('mkdir -p %(path)s/packages' % env, pty=True)
+    put('%(release)s.tar.gz' % env, '%(path)s/packages/' % env)
+    run('cd %(path)s/releases/%(release)s && tar zxf ../../packages/%(release)s.tar.gz' % env, pty=True)
+    local('rm %(release)s.tar.gz' % env)
+
+def find_python():
+    "Finds where virtualenv Python stuff is"
+    print ">>> find Python paths"
+    require('path', provided_by=servers)
+    env.python = '%(path)s/ve/bin/python' % env
+    env.pip = '%(path)s/ve/bin/pip' % env
+    env.site_packages = run('%(python)s -c "from distutils.sysconfig import get_python_lib; print get_python_lib()"' % env)
+
+def upload_vhost_sample():
+    "Create and upload Apache virtual host configuration sample"
+    print ">>> upload vhost sample"
+    require('path', 'project_name', 'user', provided_by=servers)
+    require('access_log', 'error_log', 'server_admin', 'server_name', provided_by=servers)
+    require('site_packages', provided_by=[find_python])
+    files.upload_template('%(project_name)s.vhost.template' % env, '%(path)s/%(project_name)s.vhost' % env, context=env)
+
+def upload_wsgi_script():
+    "Create and upload a wsgi script sample"
+    print ">>> upload wsgi script sample"
+    require('path', 'project_name', provided_by=servers)
+    require('python', 'site_packages', provided_by=[find_python])
+    files.upload_template('%(project_name)s.wsgi.template' % env, '%(path)s/%(project_name)s.wsgi' % env, context=env)
+    run('chmod ug+x %(path)s/%(project_name)s.wsgi' % env)
+
+def install_requirements():
+    "Install the required packages from the requirements file using pip"
+    print '>>> install requirements'
+    require('path', provided_by=servers)
+    require('release', provided_by=[deploy])
+    require('pip', provided_by=[find_python])
+    run('%(pip)s install -r %(path)s/releases/%(release)s/requirements.txt' % env, pty=True)
+
+def secret_key():
+    """Generates a new SECRET_KEY."""
+    from random import Random
+    import string
+
+    r = Random()
+    return "".join(r.choice(string.printable) for i in range(64))
+
+def upload_default_localsettings():
+    "Uploads localsettings.py with media paths and stuff"
+    print ">>> upload default localsettings.py"
+    require('path', provided_by=servers)
+
+    env.secret_key = secret_key()
+    files.upload_template('%(project_name)s/localsettings.py.template' % env, '%(path)s/localsettings.py' % env, context=env)
+
+def copy_localsettings():
+    "Copy localsettings.py from root directory to release directory (if this file exists)"
+    print ">>> copy localsettings"
+    require('path', 'project_name', provided_by=servers)
+    require('release', provided_by=[deploy])
+
+    with settings(warn_only=True):
+        run('cp %(path)s/localsettings.py %(path)s/releases/%(release)s/%(project_name)s' % env)
+
+def symlink_current_release():
+    "Symlink our current release"
+    print '>>> symlink current release'
+    require('path', provided_by=servers)
+    require('release', provided_by=[deploy])
+    with cd(env.path):
+        run('rm releases/previous; mv releases/current releases/previous')
+        run('ln -s %(release)s releases/current' % env)
+
+def collectstatic():
+    """Runs collectstatic management command from Django staticfiles."""
+    print '>>> collectstatic'
+    require('path', 'project_name', provided_by=servers)
+    require('python', provided_by=[find_python])
+    with cd('%(path)s/releases/current/%(project_name)s' % env):
+        run('%(python)s manage.py collectstatic --noinput' % env, pty=True)
+
+def migrate():
+    "Update the database"
+    print '>>> migrate'
+    require('path', 'project_name', provided_by=servers)
+    require('python', provided_by=[find_python])
+    with cd('%(path)s/releases/current/%(project_name)s' % env):
+        run('%(python)s manage.py syncdb --noinput' % env, pty=True)
+        if env.use_south:
+            run('%(python)s manage.py migrate' % env, pty=True)
+
+def restart_webserver():
+    "Restart the web server"
+    print '>>> restart webserver'
+    require('path', 'project_name', provided_by=servers)
+    run('touch %(path)s/%(project_name)s.wsgi' % env)
diff --git a/koedquiz.vhost.template b/koedquiz.vhost.template
new file mode 100644 (file)
index 0000000..3caae7f
--- /dev/null
@@ -0,0 +1,31 @@
+<VirtualHost *:80>
+    ServerName "%(server_name)s"
+    ServerAdmin "%(server_admin)s"
+
+    WSGIDaemonProcess %(project_name)s user=%(user)s group=%(user)s processes=2 threads=15 display-name=%%{GROUP} python-path=%(site_packages)s
+    WSGIProcessGroup %(project_name)s
+
+    WSGIScriptAlias / %(path)s/%(project_name)s.wsgi
+    <Directory %(path)s>
+        Order allow,deny
+        allow from all
+    </Directory>
+
+    Alias /media %(path)s/media
+    <Directory %(path)s/media>
+        Options Indexes
+        Order allow,deny
+        Allow from all
+    </Directory>
+        
+    Alias /static %(path)s/releases/current/%(project_name)s/static
+    <Directory %(path)s/releases/current/%(project_name)s/static>
+        Options Indexes
+        Order allow,deny
+        Allow from all
+    </Directory>
+
+    LogLevel warn
+    ErrorLog /var/log/apache2/%(error_log)s
+    CustomLog /var/log/apache2/%(access_log)s combined
+</VirtualHost>
diff --git a/koedquiz.wsgi.template b/koedquiz.wsgi.template
new file mode 100644 (file)
index 0000000..af1781c
--- /dev/null
@@ -0,0 +1,26 @@
+#!%(python)s
+import site
+site.addsitedir('%(site_packages)s')
+
+import os
+from os.path import abspath, dirname, join
+import sys
+
+# Redirect sys.stdout to sys.stderr for bad libraries like geopy that use
+# print statements for optional import exceptions.
+sys.stdout = sys.stderr
+
+# Add apps and lib directories to PYTHONPATH
+sys.path = [
+       '%(path)s/releases/current/%(project_name)s',
+       '%(path)s/releases/current',
+       '%(path)s/releases/current/apps',
+       '%(path)s/releases/current/lib',
+    # add paths to submodules here
+] + sys.path
+
+# Run Django
+os.environ['DJANGO_SETTINGS_MODULE'] = 'settings'
+
+from django.core.handlers.wsgi import WSGIHandler
+application = WSGIHandler()
diff --git a/koedquiz/__init__.py b/koedquiz/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/koedquiz/localsettings.py.template b/koedquiz/localsettings.py.template
new file mode 100644 (file)
index 0000000..3765080
--- /dev/null
@@ -0,0 +1,27 @@
+# This template is uploaded by `fab setup`.
+# You should fill out the details in the version deployed on the server.
+
+
+ADMINS = (
+    #('Name', 'E-mail'),
+)
+
+MANAGERS = (
+    #('Name', 'E-mail'),
+)
+
+# on
+DATABASES = {
+    'default': {
+        'ENGINE': 'django.db.backends.', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
+        'NAME': '', # Or path to database file if using sqlite3.
+        'USER': '',                      # Not used with sqlite3.
+        'PASSWORD': '',                  # Not used with sqlite3.
+        'HOST': '',                      # Set to empty string for localhost. Not used with sqlite3.
+        'PORT': '',                      # Set to empty string for default. Not used with sqlite3.
+    }
+}
+
+
+SECRET_KEY = %(secret_key)r
+MEDIA_ROOT = '%(path)s/media/'
diff --git a/koedquiz/manage.py b/koedquiz/manage.py
new file mode 100755 (executable)
index 0000000..eb91c2d
--- /dev/null
@@ -0,0 +1,26 @@
+#!/usr/bin/env python
+import os.path
+import sys
+
+ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+# Add apps and lib directories to PYTHONPATH
+sys.path = [
+    os.path.join(ROOT, 'apps'),
+    os.path.join(ROOT, 'lib'),
+    # add /lib/* paths here for submodules
+] + sys.path
+
+from django.core.management import execute_manager
+import imp
+try:
+    imp.find_module('settings') # Assumed to be in the same directory.
+except ImportError:
+    import sys
+    sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n" % __file__)
+    sys.exit(1)
+
+import settings
+
+if __name__ == "__main__":
+    execute_manager(settings)
diff --git a/koedquiz/settings.py b/koedquiz/settings.py
new file mode 100644 (file)
index 0000000..52cd99a
--- /dev/null
@@ -0,0 +1,156 @@
+# -*- coding: utf-8 -*-
+# Django settings for koedquiz project.
+import os.path
+
+PROJECT_DIR = os.path.abspath(os.path.dirname(__file__))
+
+DEBUG = False
+TEMPLATE_DEBUG = DEBUG
+
+ADMINS = [
+    # ('Your Name', 'your_email@domain.com'),
+]
+
+MANAGERS = ADMINS
+
+DATABASES = {
+    'default': {
+        'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
+        'NAME': os.path.join(PROJECT_DIR, 'dev.db'), # Or path to database file if using sqlite3.
+        'USER': '',                      # Not used with sqlite3.
+        'PASSWORD': '',                  # Not used with sqlite3.
+        'HOST': '',                      # Set to empty string for localhost. Not used with sqlite3.
+        'PORT': '',                      # Set to empty string for default. Not used with sqlite3.
+    }
+}
+
+# Local time zone for this installation. Choices can be found here:
+# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
+# although not all choices may be available on all operating systems.
+# On Unix systems, a value of None will cause Django to use the same
+# timezone as the operating system.
+# If running in a Windows environment this must be set to the same as your
+# system time zone.
+TIME_ZONE = None
+
+# Language code for this installation. All choices can be found here:
+# http://www.i18nguy.com/unicode/language-identifiers.html
+LANGUAGE_CODE = 'pl'
+
+SITE_ID = 1
+
+# If you set this to False, Django will make some optimizations so as not
+# to load the internationalization machinery.
+USE_I18N = True
+
+# If you set this to False, Django will not format dates, numbers and
+# calendars according to the current locale
+USE_L10N = True
+
+# Absolute filesystem path to the directory that will hold user-uploaded files.
+# Example: "/home/media/media.lawrence.com/media/"
+MEDIA_ROOT = os.path.join(PROJECT_DIR, '../media')
+
+# URL that handles the media served from MEDIA_ROOT. Make sure to use a
+# trailing slash.
+# Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
+MEDIA_URL = '/media/'
+
+# Absolute path to the directory static files should be collected to.
+# Don't put anything in this directory yourself; store your static files
+# in apps' "static/" subdirectories and in STATICFILES_DIRS.
+# Example: "/home/media/media.lawrence.com/static/"
+STATIC_ROOT = os.path.join(PROJECT_DIR, '../static')
+
+# URL prefix for static files.
+# Example: "http://media.lawrence.com/static/"
+STATIC_URL = '/static/'
+
+# URL prefix for admin static files -- CSS, JavaScript and images.
+# Make sure to use a trailing slash.
+# Examples: "http://foo.com/static/admin/", "/static/admin/".
+ADMIN_MEDIA_PREFIX = '/static/admin/'
+
+# Additional locations of static files
+STATICFILES_DIRS = (
+    # Put strings here, like "/home/html/static" or "C:/www/django/static".
+    # Always use forward slashes, even on Windows.
+    # Don't forget to use absolute paths, not relative paths.
+)
+
+# List of finder classes that know how to find static files in
+# various locations.
+STATICFILES_FINDERS = (
+    'django.contrib.staticfiles.finders.FileSystemFinder',
+    'django.contrib.staticfiles.finders.AppDirectoriesFinder',
+#    'django.contrib.staticfiles.finders.DefaultStorageFinder',
+)
+
+# List of callables that know how to import templates from various sources.
+TEMPLATE_LOADERS = [
+    'django.template.loaders.filesystem.Loader',
+    'django.template.loaders.app_directories.Loader',
+#     'django.template.loaders.eggs.Loader',
+]
+
+MIDDLEWARE_CLASSES = [
+    'django.middleware.common.CommonMiddleware',
+    'django.contrib.sessions.middleware.SessionMiddleware',
+    'django.middleware.csrf.CsrfViewMiddleware',
+    'django.contrib.auth.middleware.AuthenticationMiddleware',
+    'django.contrib.messages.middleware.MessageMiddleware',
+]
+
+ROOT_URLCONF = 'koedquiz.urls'
+
+TEMPLATE_DIRS = [
+    # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
+    # Always use forward slashes, even on Windows.
+    # Don't forget to use absolute paths, not relative paths.
+    os.path.join(PROJECT_DIR, 'templates'),
+]
+
+INSTALLED_APPS = [
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    'django.contrib.sites',
+    'django.contrib.messages',
+    'django.contrib.staticfiles',
+
+    'django.contrib.admin',
+    'django.contrib.admindocs',
+
+    'south',
+
+    'quiz',
+]
+
+# A sample logging configuration. The only tangible logging
+# performed by this configuration is to send an email to
+# the site admins on every HTTP 500 error.
+# See http://docs.djangoproject.com/en/dev/topics/logging for
+# more details on how to customize your logging configuration.
+LOGGING = {
+    'version': 1,
+    'disable_existing_loggers': False,
+    'handlers': {
+        'mail_admins': {
+            'level': 'ERROR',
+            'class': 'django.utils.log.AdminEmailHandler'
+        }
+    },
+    'loggers': {
+        'django.request': {
+            'handlers': ['mail_admins'],
+            'level': 'ERROR',
+            'propagate': True,
+        },
+    }
+}
+
+# Load localsettings, if they exist
+try:
+    from localsettings import *
+except ImportError:
+    pass
diff --git a/koedquiz/templates/base.html b/koedquiz/templates/base.html
new file mode 100644 (file)
index 0000000..c6ffd3c
--- /dev/null
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+    <title>{% block "title" %}{% endblock %}</title>
+</head>
+
+<body>
+    {% block "body" %}{% endblock %}
+</body>
+
+</html>
diff --git a/koedquiz/templates/home.html b/koedquiz/templates/home.html
new file mode 100644 (file)
index 0000000..5c856b8
--- /dev/null
@@ -0,0 +1,7 @@
+{% extends "base.html" %}
+
+{% block "body" %}
+
+<a href="{{ quiz.get_absolute_url }}">Start quiz!</a>
+
+{% endblock %}
diff --git a/koedquiz/urls.py b/koedquiz/urls.py
new file mode 100644 (file)
index 0000000..51fd0d8
--- /dev/null
@@ -0,0 +1,13 @@
+from django.conf.urls.defaults import patterns, include, url
+
+from django.contrib import admin
+admin.autodiscover()
+
+urlpatterns = patterns('',
+    url(r'^$', 'koedquiz.views.home', name='main_page'),
+
+    url(r'^quiz/', include('quiz.urls')),
+
+    url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
+    url(r'^admin/', include(admin.site.urls)),
+)
diff --git a/koedquiz/views.py b/koedquiz/views.py
new file mode 100644 (file)
index 0000000..d554715
--- /dev/null
@@ -0,0 +1,10 @@
+from django.shortcuts import render
+#from django.contrib.sites import Site
+
+from django.conf import settings
+from quiz.models import Quiz
+
+
+def home(request):
+    quiz = Quiz.current()
+    return render(request, "home.html", locals())
diff --git a/lib/git-archive-all.sh b/lib/git-archive-all.sh
new file mode 100644 (file)
index 0000000..95e8582
--- /dev/null
@@ -0,0 +1,208 @@
+#!/bin/bash -
+#
+# File:        git-archive-all.sh
+#
+# Description: A utility script that builds an archive file(s) of all
+#              git repositories and submodules in the current path.
+#              Useful for creating a single tarfile of a git super-
+#              project that contains other submodules.
+#
+# Examples:    Use git-archive-all.sh to create archive distributions
+#              from git repositories. To use, simply do:
+#
+#                  cd $GIT_DIR; git-archive-all.sh
+#
+#              where $GIT_DIR is the root of your git superproject.
+#
+# License:     GPL3
+#
+###############################################################################
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+#
+###############################################################################
+
+# DEBUGGING
+set -e
+set -C # noclobber
+
+# TRAP SIGNALS
+trap 'cleanup' QUIT EXIT
+
+# For security reasons, explicitly set the internal field separator
+# to newline, space, tab
+OLD_IFS=$IFS
+IFS='
+       '
+
+function cleanup () {
+    rm -f $TMPFILE
+    rm -f $TOARCHIVE
+    IFS="$OLD_IFS"
+}
+
+function usage () {
+    echo "Usage is as follows:"
+    echo
+    echo "$PROGRAM <--version>"
+    echo "    Prints the program version number on a line by itself and exits."
+    echo
+    echo "$PROGRAM <--usage|--help|-?>"
+    echo "    Prints this usage output and exits."
+    echo
+    echo "$PROGRAM [--format <fmt>] [--prefix <path>] [--separate|-s] [output_file]"
+    echo "    Creates an archive for the entire git superproject, and its submodules"
+    echo "    using the passed parameters, described below."
+    echo
+    echo "    If '--format' is specified, the archive is created with the named"
+    echo "    git archiver backend. Obviously, this must be a backend that git archive"
+    echo "    understands. The format defaults to 'tar' if not specified."
+    echo
+    echo "    If '--prefix' is specified, the archive's superproject and all submodules"
+    echo "    are created with the <path> prefix named. The default is to not use one."
+    echo
+    echo "    If '--separate' or '-s' is specified, individual archives will be created"
+    echo "    for each of the superproject itself and its submodules. The default is to"
+    echo "    concatenate individual archives into one larger archive."
+    echo
+    echo "    If 'output_file' is specified, the resulting archive is created as the"
+    echo "    file named. This parameter is essentially a path that must be writeable."
+    echo "    When combined with '--separate' ('-s') this path must refer to a directory."
+    echo "    Without this parameter or when combined with '--separate' the resulting"
+    echo "    archive(s) are named with a dot-separated path of the archived directory and"
+    echo "    a file extension equal to their format (e.g., 'superdir.submodule1dir.tar')."
+}
+
+function version () {
+    echo "$PROGRAM version $VERSION"
+}
+
+# Internal variables and initializations.
+readonly PROGRAM=`basename "$0"`
+readonly VERSION=0.2
+
+OLD_PWD="`pwd`"
+TMPDIR=${TMPDIR:-/tmp}
+TMPFILE=`mktemp "$TMPDIR/$PROGRAM.XXXXXX"` # Create a place to store our work's progress
+TOARCHIVE=`mktemp "$TMPDIR/$PROGRAM.toarchive.XXXXXX"`
+OUT_FILE=$OLD_PWD # assume "this directory" without a name change by default
+SEPARATE=0
+
+FORMAT=tar
+PREFIX=
+TREEISH=HEAD
+
+# RETURN VALUES/EXIT STATUS CODES
+readonly E_BAD_OPTION=254
+readonly E_UNKNOWN=255
+
+# Process command-line arguments.
+while test $# -gt 0; do
+    case $1 in
+        --format )
+            shift
+            FORMAT="$1"
+            shift
+            ;;
+
+        --prefix )
+            shift
+            PREFIX="$1"
+            shift
+            ;;
+
+        --separate | -s )
+            shift
+            SEPARATE=1
+            ;;
+
+        --version )
+            version
+            exit
+            ;;
+
+        -? | --usage | --help )
+            usage
+            exit
+            ;;
+
+        -* )
+            echo "Unrecognized option: $1" >&2
+            usage
+            exit $E_BAD_OPTION
+            ;;
+
+        * )
+            break
+            ;;
+    esac
+done
+
+if [ ! -z "$1" ]; then
+    OUT_FILE="$1"
+    shift
+fi
+
+# Validate parameters; error early, error often.
+if [ $SEPARATE -eq 1 -a ! -d $OUT_FILE ]; then
+    echo "When creating multiple archives, your destination must be a directory."
+    echo "If it's not, you risk being surprised when your files are overwritten."
+    exit
+elif [ `git config -l | grep -q '^core\.bare=false'; echo $?` -ne 0 ]; then
+    echo "$PROGRAM must be run from a git working copy (i.e., not a bare repository)."
+    exit
+fi
+
+# Create the superproject's git archive
+git archive --format=$FORMAT --prefix="$PREFIX" $TREEISH > $TMPDIR/$(basename $(pwd)).$FORMAT
+echo $TMPDIR/$(basename $(pwd)).$FORMAT >| $TMPFILE # clobber on purpose
+superfile=`head -n 1 $TMPFILE`
+
+# find all '.git' dirs, these show us the remaining to-be-archived dirs
+find . -name '.git' -type d -print | sed -e 's/^\.\///' -e 's/\.git$//' | (grep -v '^$' || echo -n) >> $TOARCHIVE
+
+while read path; do
+    TREEISH=$(git submodule | grep "^ .*${path%/} " | cut -d ' ' -f 2) # git submodule does not list trailing slashes in $path
+    cd "$path"
+    git archive --format=$FORMAT --prefix="${PREFIX}$path" ${TREEISH:-HEAD} > "$TMPDIR"/"$(echo "$path" | sed -e 's/\//./g')"$FORMAT
+    if [ $FORMAT == 'zip' ]; then
+        # delete the empty directory entry; zipped submodules won't unzip if we don't do this
+        zip -d "$(tail -n 1 $TMPFILE)" "${PREFIX}${path%/}" >/dev/null # remove trailing '/'
+    fi
+    echo "$TMPDIR"/"$(echo "$path" | sed -e 's/\//./g')"$FORMAT >> $TMPFILE
+    cd "$OLD_PWD"
+done < $TOARCHIVE
+
+# Concatenate archives into a super-archive.
+if [ $SEPARATE -eq 0 ]; then
+    if [ $FORMAT == 'tar' ]; then
+        sed -e '1d' $TMPFILE | while read file; do
+            tar --concatenate -f "$superfile" "$file" && rm -f "$file"
+        done
+    elif [ $FORMAT == 'zip' ]; then
+        sed -e '1d' $TMPFILE | while read file; do
+            # zip incorrectly stores the full path, so cd and then grow
+            cd `dirname "$file"`
+            zip -g "$superfile" `basename "$file"` && rm -f "$file"
+        done
+        cd "$OLD_PWD"
+    fi
+
+    echo "$superfile" >| $TMPFILE # clobber on purpose
+fi
+
+while read file; do
+    mv "$file" "$OUT_FILE"
+done < $TMPFILE
diff --git a/requirements-dev.txt b/requirements-dev.txt
new file mode 100644 (file)
index 0000000..aec394f
--- /dev/null
@@ -0,0 +1 @@
+django-debug-toolbar
diff --git a/requirements.txt b/requirements.txt
new file mode 100644 (file)
index 0000000..20e6116
--- /dev/null
@@ -0,0 +1,3 @@
+# Django basics
+django>=1.3,<1.4
+South>=0.7