Commits

Konstantin Mochalov committed a8803e2 Merge

Merge branch 'master' into openshift

Comments (0)

Files changed (16)

+*.pyc
+*.sqlite3
+#!/usr/bin/env python
+import os
+import sys
+
+if __name__ == "__main__":
+    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_tasks.settings")
+
+    from django.core.management import execute_from_command_line
+
+    execute_from_command_line(sys.argv)

tasks/__init__.py

Empty file added.

tasks/decorators.py

+import json
+
+def json_request_body(func):
+    """
+    Decorator for views that parses JSON request body and puts parsed data to request.data.
+    In case of invalid json returns HttpResponseBadRequest.
+    """
+    def decorated(request, *args, **kwargs):
+        try:
+            request.data = json.loads(request.body)
+        except ValueError:
+            return HttpResponseBadRequest("Invalid json")
+
+        return func(request, *args, **kwargs)
+    return decorated

tasks/fixtures/initial_data.json

+[
+{
+  "pk": 1, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2012-12-01T09:30:07", 
+    "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
+  }
+},
+{
+  "pk": 2, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2012-02-10T11:05:17", 
+    "description": "Mauris rhoncus orci non massa blandit blandit."
+  }
+},
+{
+  "pk": 3, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2013-07-29T06:17:30", 
+    "description": "Duis scelerisque nisl at mi varius, laoreet consectetur lectus volutpat."
+  }
+},
+{
+  "pk": 4, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2013-02-10T05:15:57", 
+    "description": "Fusce pellentesque diam ut semper interdum."
+  }
+},
+{
+  "pk": 5, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2013-08-12T11:44:37", 
+    "description": "Proin ultricies neque at erat bibendum, non molestie urna vehicula."
+  }
+},
+{
+  "pk": 6, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2013-03-19T11:28:53", 
+    "description": "Pellentesque lacinia ipsum at felis scelerisque adipiscing."
+  }
+},
+{
+  "pk": 7, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2013-03-21T13:46:38", 
+    "description": "Proin diam eros, consectetur nec congue in, ornare at mi. Donec a cursus lorem, id gravida ligula. Integer vulputate feugiat mi ac facilisis. Donec accumsan quam ut tellus mollis, eget commodo est pretium. Nunc placerat orci et vulputate gravida. Integer laoreet elit at velit lobortis, id rutrum nisi tincidunt. In condimentum vitae ligula luctus pharetra. Nam nec purus odio. In hac habitasse platea dictumst."
+  }
+},
+{
+  "pk": 8, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2013-08-31T14:57:46", 
+    "description": "Vivamus eu enim sit amet neque venenatis viverra."
+  }
+},
+{
+  "pk": 9, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2012-07-09T01:37:59", 
+    "description": "Mauris tristique leo placerat, sollicitudin mauris sed, feugiat arcu."
+  }
+},
+{
+  "pk": 10, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2012-11-05T03:36:44", 
+    "description": "Phasellus mattis sem euismod, facilisis nibh et, auctor ligula."
+  }
+},
+{
+  "pk": 11, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2013-04-16T12:57:32", 
+    "description": "Pellentesque fermentum dolor non arcu molestie consequat."
+  }
+},
+{
+  "pk": 12, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2013-07-15T16:21:31", 
+    "description": "Praesent molestie nulla vitae magna tempor euismod."
+  }
+},
+{
+  "pk": 13, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2013-06-11T05:53:18", 
+    "description": "Nullam viverra lacus a elit rhoncus laoreet."
+  }
+},
+{
+  "pk": 14, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2012-03-28T16:36:05", 
+    "description": "Curabitur hendrerit risus ut neque aliquam mattis."
+  }
+},
+{
+  "pk": 15, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2013-06-05T19:42:37", 
+    "description": "Cras vitae nisi ultricies, sodales dolor vitae, interdum dui."
+  }
+},
+{
+  "pk": 16, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2013-05-21T13:02:25", 
+    "description": "Nunc scelerisque ligula in mauris egestas placerat."
+  }
+},
+{
+  "pk": 17, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2013-06-03T06:23:51", 
+    "description": "Nullam quis sapien ac sem porta semper ac ut quam."
+  }
+},
+{
+  "pk": 18, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2012-05-27T20:04:54", 
+    "description": "Praesent consectetur dolor nec lacus iaculis, vel feugiat diam sodales. Aenean massa metus, viverra porta urna eu, hendrerit elementum eros. Sed pretium, nisl vitae congue pulvinar, est risus elementum odio, ac dapibus nibh risus sed enim. Donec eleifend, eros non posuere blandit, nibh nisi aliquam magna, ut molestie urna diam a leo. In et adipiscing orci, a elementum libero. Nam sit amet libero id diam rhoncus egestas ac in tellus. Morbi felis purus, tempor et enim in, rhoncus feugiat felis. Phasellus quis risus scelerisque purus congue cursus. Ut non nisl augue. In congue sem congue dolor tempus, et pretium libero feugiat. Sed lectus risus, tristique in elit in, tempor iaculis turpis. Curabitur posuere in est in interdum. Interdum et malesuada fames ac ante ipsum primis in faucibus. Cras interdum mauris vel arcu bibendum iaculis. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Etiam sit amet iaculis ante, eget iaculis nunc."
+  }
+},
+{
+  "pk": 19, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2013-03-24T15:55:39", 
+    "description": "Aliquam aliquet turpis non eros venenatis lobortis."
+  }
+},
+{
+  "pk": 20, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2012-03-27T05:57:39", 
+    "description": "Etiam eu erat ut lacus eleifend tincidunt."
+  }
+},
+{
+  "pk": 21, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2012-05-12T08:32:24", 
+    "description": "Aenean ultrices enim non tortor venenatis sodales vel at sem."
+  }
+},
+{
+  "pk": 22, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2012-06-22T06:29:15", 
+    "description": "Aliquam id erat eget neque malesuada hendrerit a quis enim."
+  }
+},
+{
+  "pk": 23, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2013-05-11T17:21:34", 
+    "description": "Nullam tempor velit ut purus pharetra, quis varius erat porta."
+  }
+},
+{
+  "pk": 24, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2013-05-31T20:11:18", 
+    "description": "Praesent tristique diam ut augue dapibus, ut convallis dolor euismod."
+  }
+},
+{
+  "pk": 25, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2013-09-06T03:25:20", 
+    "description": "Fusce ac tempus nibh. Maecenas neque magna, auctor vitae sapien vel, ornare ultricies tortor. Duis varius lectus vestibulum lacus dapibus tempus. Donec mattis quis turpis vitae gravida. Sed elementum odio rutrum, ultrices augue dictum, molestie odio. Mauris viverra sodales molestie. Vivamus vestibulum iaculis tellus nec condimentum. Nunc interdum urna libero, vel pulvinar diam porta sed. Nullam quis nisl id nisl porta suscipit id ut justo."
+  }
+},
+{
+  "pk": 26, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2013-02-16T14:18:58", 
+    "description": "Duis commodo massa vitae eros accumsan, non bibendum quam commodo."
+  }
+},
+{
+  "pk": 27, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2013-05-28T04:22:50", 
+    "description": "Nulla tristique sapien at ligula dapibus, quis tincidunt nisi tempus."
+  }
+},
+{
+  "pk": 28, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2013-03-14T03:44:35", 
+    "description": "In nec purus ut libero suscipit dapibus."
+  }
+},
+{
+  "pk": 29, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2012-06-01T21:48:02", 
+    "description": "Suspendisse eget diam et mi consectetur commodo."
+  }
+},
+{
+  "pk": 30, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2012-08-09T23:36:21", 
+    "description": "Donec sit amet enim in arcu ornare ultricies sit amet eget sem."
+  }
+},
+{
+  "pk": 31, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2012-10-07T05:16:23", 
+    "description": "Vivamus tristique diam varius nunc adipiscing lacinia eu a nisl."
+  }
+},
+{
+  "pk": 32, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2012-04-16T19:24:35", 
+    "description": "Pellentesque ut turpis id metus faucibus tempor placerat ac elit."
+  }
+},
+{
+  "pk": 33, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2012-02-23T05:17:28", 
+    "description": "Cras ut est hendrerit, ultricies nisl nec, sagittis massa."
+  }
+},
+{
+  "pk": 34, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2013-03-03T08:57:14", 
+    "description": "In vitae diam tincidunt, accumsan diam eu, varius nulla."
+  }
+},
+{
+  "pk": 35, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2013-08-21T18:33:17", 
+    "description": "Integer ut neque congue, feugiat lorem ac, elementum eros."
+  }
+},
+{
+  "pk": 36, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2013-05-11T03:27:50", 
+    "description": "Vestibulum vitae nunc iaculis, egestas nisi vel, molestie sem."
+  }
+},
+{
+  "pk": 37, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2013-03-15T15:49:19", 
+    "description": "Proin iaculis massa tincidunt, rutrum augue non, malesuada velit. Pellentesque vulputate velit id lectus pulvinar consequat. Nulla at justo pharetra, hendrerit sapien sit amet, iaculis lacus. In hac habitasse platea dictumst. Interdum et malesuada fames ac ante ipsum primis in faucibus. Sed iaculis accumsan turpis ut adipiscing. Aliquam eget vulputate nibh. Cras tempus ultricies placerat. Cras et lacus metus. In tellus risus, lobortis eu neque a, ornare fringilla quam. Donec sed vulputate tortor, nec ultrices tortor. Suspendisse sit amet condimentum mauris. Morbi ornare sit amet leo non suscipit. Phasellus sit amet erat blandit, placerat tellus ut, pharetra lorem."
+  }
+},
+{
+  "pk": 38, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2013-02-17T10:49:37", 
+    "description": "Sed tincidunt nunc in libero sollicitudin aliquet."
+  }
+},
+{
+  "pk": 39, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2012-07-16T07:42:22", 
+    "description": "Aliquam vel nisi egestas est elementum suscipit."
+  }
+},
+{
+  "pk": 40, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2012-10-05T22:05:28", 
+    "description": "Integer at dolor at nisi rutrum condimentum."
+  }
+},
+{
+  "pk": 41, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2013-01-14T08:17:37", 
+    "description": "Nullam scelerisque elit id dapibus auctor."
+  }
+},
+{
+  "pk": 42, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2013-09-05T00:19:37", 
+    "description": "Sed vitae tortor in lacus faucibus vehicula."
+  }
+},
+{
+  "pk": 43, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2013-03-21T04:55:25", 
+    "description": "Integer posuere tortor interdum, pulvinar ante nec, sodales sapien."
+  }
+},
+{
+  "pk": 44, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2012-08-06T20:19:31", 
+    "description": "Ut egestas risus a sagittis commodo."
+  }
+},
+{
+  "pk": 45, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2012-03-29T02:55:51", 
+    "description": "Vestibulum et diam sit amet elit faucibus malesuada."
+  }
+},
+{
+  "pk": 46, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2013-08-08T11:51:01", 
+    "description": "Vestibulum at risus ultricies, sagittis lorem nec, posuere leo."
+  }
+},
+{
+  "pk": 47, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2013-02-27T17:55:39", 
+    "description": "Donec ullamcorper massa id arcu lacinia, a pulvinar ante vulputate."
+  }
+},
+{
+  "pk": 48, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2012-02-16T05:54:04", 
+    "description": "Donec posuere mi non arcu porttitor, in sollicitudin orci luctus."
+  }
+},
+{
+  "pk": 49, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2013-04-11T14:55:42", 
+    "description": "Aenean aliquam leo eleifend neque suscipit, at euismod arcu dignissim."
+  }
+},
+{
+  "pk": 50, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2013-05-17T17:41:40", 
+    "description": "Phasellus sed purus at nisi aliquam suscipit nec ultricies magna."
+  }
+},
+{
+  "pk": 51, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2013-07-25T13:47:53", 
+    "description": "Nunc at metus vulputate, luctus risus non, rhoncus nisl."
+  }
+},
+{
+  "pk": 52, 
+  "model": "tasks.task", 
+  "fields": {
+    "date_start": "2012-11-05T14:40:27", 
+    "description": "Suspendisse hendrerit congue nibh, sed iaculis mi molestie non. Donec at egestas risus. Integer vehicula elit in leo vestibulum condimentum. Mauris sed egestas lorem, in cursus ligula. Mauris in dolor euismod magna rutrum posuere a vitae mauris. Etiam at ornare mi, vitae aliquet risus. Nullam non condimentum tortor. Curabitur sodales euismod ante, ac faucibus quam consectetur quis. Vestibulum a quam quis est pellentesque facilisis dapibus at mauris. Phasellus viverra justo in hendrerit pretium."
+  }
+}
+]
+from django.db import models
+import datetime
+
+class ValidationError(Exception):
+    pass
+
+class Task(models.Model):
+    description = models.TextField()
+    date_start = models.DateTimeField(db_index=True)
+
+    def to_api(self):
+        return {
+            'id': self.id,
+            'description': self.description,
+            'date_start': self.date_start.isoformat(),
+        }
+
+    def update_from_api(self, data):
+        try:
+            date = datetime.datetime.strptime(data.get('date_start', ''), "%Y-%m-%dT%H:%M:%S")
+        except ValueError:
+            raise ValidationError("Invalid date")
+
+        if data.get('description', '').strip() == '':
+            raise ValidationError("Invalid description")
+
+        self.description = data['description']
+        self.date_start = date

tasks/sql/task.mysql.sql

+ALTER TABLE `tasks_task` ADD INDEX `index_description` (`description`(64));

tasks/static/tasks.css

+.sortable-columns th {
+	padding-left: 14px !important;
+}
+.sort-desc {
+    background:no-repeat left center url(data:image/gif;base64,R0lGODlhCgAKALMAAHFxcYKCgp2dnaampq+vr83NzeHh4f///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAAAgAIf/8SUNDUkdCRzEwMTIAAAUwYXBwbAIgAABtbnRyUkdCIFhZWiAH2QACABkACwAaAAthY3NwQVBQTAAAAABhcHBsAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLWFwcGwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAtkc2NtAAABCAAAAvJkZXNjAAAD/AAAAG9nWFlaAAAEbAAAABR3dHB0AAAEgAAAABRyWFlaAAAElAAAABRiWFlaAAAEqAAAABRyVFJDAAAEvAAAAA5jcHJ0AAAEzAAAADhjaGFkAAAFBAAAACxn/1RSQwAABLwAAAAOYlRSQwAABLwAAAAObWx1YwAAAAAAAAARAAAADGVuVVMAAAAmAAACfmVzRVMAAAAmAAABgmRhREsAAAAuAAAB6mRlREUAAAAsAAABqGZpRkkAAAAoAAAA3GZyRlUAAAAoAAABKml0SVQAAAAoAAACVm5sTkwAAAAoAAACGG5iTk8AAAAmAAABBHB0QlIAAAAmAAABgnN2U0UAAAAmAAABBGphSlAAAAAaAAABUmtvS1IAAAAWAAACQHpoVFcAAAAWAAABbHpoQ04AAAAWAAAB1HJ1UlUAAAAiAAACpHBsUEwAAAAsAAACxgBZAGwAZQBpAG4AZf8AbgAgAFIARwBCAC0AcAByAG8AZgBpAGkAbABpAEcAZQBuAGUAcgBpAHMAawAgAFIARwBCAC0AcAByAG8AZgBpAGwAUAByAG8AZgBpAGwAIABHAOkAbgDpAHIAaQBxAHUAZQAgAFIAVgBCTgCCLAAgAFIARwBCACAw1zDtMNUwoTCkMOuQGnUoACAAUgBHAEIAIIJyX2ljz4/wAFAAZQByAGYAaQBsACAAUgBHAEIAIABHAGUAbgDpAHIAaQBjAG8AQQBsAGwAZwBlAG0AZQBpAG4AZQBzACAAUgBHAEIALQBQAHIAbwBmAGkAbGZukBoAIABSAEcAQgAgY8+P8GX/h072AEcAZQBuAGUAcgBlAGwAIABSAEcAQgAtAGIAZQBzAGsAcgBpAHYAZQBsAHMAZQBBAGwAZwBlAG0AZQBlAG4AIABSAEcAQgAtAHAAcgBvAGYAaQBlAGzHfLwYACAAUgBHAEIAINUEuFzTDMd8AFAAcgBvAGYAaQBsAG8AIABSAEcAQgAgAEcAZQBuAGUAcgBpAGMAbwBHAGUAbgBlAHIAaQBjACAAUgBHAEIAIABQAHIAbwBmAGkAbABlBB4EMQRJBDgEOQAgBD8EQAQ+BEQEOAQ7BEwAIABSAEcAQgBVAG4AaQB3AGUAcgBzAGEAbABuAHkAIABwAHIAbwBm/wBpAGwAIABSAEcAQgAAZGVzYwAAAAAAAAAUR2VuZXJpYyBSR0IgUHJvZmlsZQAAAAAAAAAAAAAAFEdlbmVyaWMgUkdCIFByb2ZpbGUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAABadQAArHMAABc0WFlaIAAAAAAAAPNSAAEAAAABFs9YWVogAAAAAAAAdE0AAD3uAAAD0FhZWiAAAAAAAAAoGgAAFZ8AALg2Y3VydgAAAAAAAAABAc0AAHRleHQAAAAAQ29weXJpZ2h0IDIwMDcgQXBwbGUgSW5jLkMsIGFsbCByaWdodHMgcmVzZXJ2ZWQuAHNmMzIAAAAAAAEMQgAABd7///MmAAAHkgAA/ZH///ui///9owAAA9wAAMBsACwAAAAACgAKAAAEJZAMIcakQZjNtyhFxwEIIRofAookUnapu26t+6KFLYe1TgQ5VwQAOw%3D%3D);
+}
+.sort-asc {
+    background:no-repeat left center url(data:image/gif;base64,R0lGODlhCgAKALMAAHFxcYKCgp2dnaampq+vr83NzeHh4f///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAAAgAIf/8SUNDUkdCRzEwMTIAAAUwYXBwbAIgAABtbnRyUkdCIFhZWiAH2QACABkACwAaAAthY3NwQVBQTAAAAABhcHBsAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLWFwcGwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAtkc2NtAAABCAAAAvJkZXNjAAAD/AAAAG9nWFlaAAAEbAAAABR3dHB0AAAEgAAAABRyWFlaAAAElAAAABRiWFlaAAAEqAAAABRyVFJDAAAEvAAAAA5jcHJ0AAAEzAAAADhjaGFkAAAFBAAAACxn/1RSQwAABLwAAAAOYlRSQwAABLwAAAAObWx1YwAAAAAAAAARAAAADGVuVVMAAAAmAAACfmVzRVMAAAAmAAABgmRhREsAAAAuAAAB6mRlREUAAAAsAAABqGZpRkkAAAAoAAAA3GZyRlUAAAAoAAABKml0SVQAAAAoAAACVm5sTkwAAAAoAAACGG5iTk8AAAAmAAABBHB0QlIAAAAmAAABgnN2U0UAAAAmAAABBGphSlAAAAAaAAABUmtvS1IAAAAWAAACQHpoVFcAAAAWAAABbHpoQ04AAAAWAAAB1HJ1UlUAAAAiAAACpHBsUEwAAAAsAAACxgBZAGwAZQBpAG4AZf8AbgAgAFIARwBCAC0AcAByAG8AZgBpAGkAbABpAEcAZQBuAGUAcgBpAHMAawAgAFIARwBCAC0AcAByAG8AZgBpAGwAUAByAG8AZgBpAGwAIABHAOkAbgDpAHIAaQBxAHUAZQAgAFIAVgBCTgCCLAAgAFIARwBCACAw1zDtMNUwoTCkMOuQGnUoACAAUgBHAEIAIIJyX2ljz4/wAFAAZQByAGYAaQBsACAAUgBHAEIAIABHAGUAbgDpAHIAaQBjAG8AQQBsAGwAZwBlAG0AZQBpAG4AZQBzACAAUgBHAEIALQBQAHIAbwBmAGkAbGZukBoAIABSAEcAQgAgY8+P8GX/h072AEcAZQBuAGUAcgBlAGwAIABSAEcAQgAtAGIAZQBzAGsAcgBpAHYAZQBsAHMAZQBBAGwAZwBlAG0AZQBlAG4AIABSAEcAQgAtAHAAcgBvAGYAaQBlAGzHfLwYACAAUgBHAEIAINUEuFzTDMd8AFAAcgBvAGYAaQBsAG8AIABSAEcAQgAgAEcAZQBuAGUAcgBpAGMAbwBHAGUAbgBlAHIAaQBjACAAUgBHAEIAIABQAHIAbwBmAGkAbABlBB4EMQRJBDgEOQAgBD8EQAQ+BEQEOAQ7BEwAIABSAEcAQgBVAG4AaQB3AGUAcgBzAGEAbABuAHkAIABwAHIAbwBm/wBpAGwAIABSAEcAQgAAZGVzYwAAAAAAAAAUR2VuZXJpYyBSR0IgUHJvZmlsZQAAAAAAAAAAAAAAFEdlbmVyaWMgUkdCIFByb2ZpbGUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAABadQAArHMAABc0WFlaIAAAAAAAAPNSAAEAAAABFs9YWVogAAAAAAAAdE0AAD3uAAAD0FhZWiAAAAAAAAAoGgAAFZ8AALg2Y3VydgAAAAAAAAABAc0AAHRleHQAAAAAQ29weXJpZ2h0IDIwMDcgQXBwbGUgSW5jLkMsIGFsbCByaWdodHMgcmVzZXJ2ZWQuAHNmMzIAAAAAAAEMQgAABd7///MmAAAHkgAA/ZH///ui///9owAAA9wAAMBsACwAAAAACgAKAAAEJRBJREKZsxQDsCSGIVzZFnYTGIqktp7fG46uzAn2TAyCMPC9QAQAOw%3D%3D);
+}
+.loading-indicator {
+	width: 16px;
+	height: 21px;
+	margin-left: 18px;
+	display: inline-block;
+	background: no-repeat left center url(data:image/gif;base64,R0lGODlhEAAQAPIAAP///wAAAMLCwkJCQgAAAGJiYoKCgpKSkiH+GkNyZWF0ZWQgd2l0aCBhamF4bG9hZC5pbmZvACH5BAAKAAAAIf8LTkVUU0NBUEUyLjADAQAAACwAAAAAEAAQAAADMwi63P4wyklrE2MIOggZnAdOmGYJRbExwroUmcG2LmDEwnHQLVsYOd2mBzkYDAdKa+dIAAAh+QQACgABACwAAAAAEAAQAAADNAi63P5OjCEgG4QMu7DmikRxQlFUYDEZIGBMRVsaqHwctXXf7WEYB4Ag1xjihkMZsiUkKhIAIfkEAAoAAgAsAAAAABAAEAAAAzYIujIjK8pByJDMlFYvBoVjHA70GU7xSUJhmKtwHPAKzLO9HMaoKwJZ7Rf8AYPDDzKpZBqfvwQAIfkEAAoAAwAsAAAAABAAEAAAAzMIumIlK8oyhpHsnFZfhYumCYUhDAQxRIdhHBGqRoKw0R8DYlJd8z0fMDgsGo/IpHI5TAAAIfkEAAoABAAsAAAAABAAEAAAAzIIunInK0rnZBTwGPNMgQwmdsNgXGJUlIWEuR5oWUIpz8pAEAMe6TwfwyYsGo/IpFKSAAAh+QQACgAFACwAAAAAEAAQAAADMwi6IMKQORfjdOe82p4wGccc4CEuQradylesojEMBgsUc2G7sDX3lQGBMLAJibufbSlKAAAh+QQACgAGACwAAAAAEAAQAAADMgi63P7wCRHZnFVdmgHu2nFwlWCI3WGc3TSWhUFGxTAUkGCbtgENBMJAEJsxgMLWzpEAACH5BAAKAAcALAAAAAAQABAAAAMyCLrc/jDKSatlQtScKdceCAjDII7HcQ4EMTCpyrCuUBjCYRgHVtqlAiB1YhiCnlsRkAAAOwAAAAAAAAAAAA==);
+}
+#tasks-table {
+	table-layout: fixed;
+}
+#tasks-table .description-cell {
+	overflow: hidden;
+	white-space: nowrap;
+	text-overflow: ellipsis;
+}

tasks/static/tasks.js

+angular.module('tasks', ['ngResource']).
+  controller('TasksCtrl', ['$scope', '$resource', function($scope, $resource) {
+    /// State
+    var isoformat = 'YYYY-MM-DDTHH:mm:ss'
+
+    $scope.tasks_api = $resource('/tasks', null, {
+      query: {method: 'GET'},
+      create: {method: 'PUT'}
+    })
+    $scope.task_api = $resource('/tasks/:id', {id: '@id'})
+
+    $scope.tasks = [] // page of tasks loaded into client
+    $scope.loading = false // if loading operation is running
+    $scope.selected_task_id = null
+    $scope.table_headers = [
+      ["id", "Id", "col-md-1"],
+      ["description", "Description", "col-md-9"],
+      ["date_start", "Start date", "col-md-2"]
+    ]
+    $scope.sort = {
+      column: 'id',
+      descending: false
+    }
+    $scope.page = 1 // active page
+    $scope.num_pages = 1 // number of pages available
+
+    $scope.edited = {
+      id: null, // 0 if creating new task
+      description: null,
+      date_start: null,
+      saving: false
+    }
+
+    /// Calculated properties
+
+    // CSS class of table column
+    $scope.table_column_class = function(column, added) {
+      return (column == $scope.sort.column && 'sort-' + ($scope.sort.descending ? 'desc' : 'asc')) + " " + added
+    }
+    // List of page numbers ([1, 2, 3, 4, ...])
+    $scope.pages = function() {
+      return _.range(1, $scope.num_pages+1)
+    }
+    // If selected task is in set of loaded tasks (on loaded page)
+    // Selected task id persists even when sort order is changed or page is
+    // switched.
+    $scope.is_selected_task_in_loaded_tasks = function() {
+      return $scope.selected_task_id &&
+        _.contains(_.map($scope.tasks, function(t) {return t.id}),
+          $scope.selected_task_id)
+    }
+
+    /// Methods
+
+    $scope.selectPage = function(p) {
+      $scope.page = p
+      $scope.reload()
+    }
+    // Select page current + offset, it ignores if it will be out of range
+    $scope.selectPageRelative = function(offset) {
+      var new_page = $scope.page + offset
+      if (new_page <= $scope.num_pages && new_page >= 1) {
+        $scope.selectPage(new_page)
+      }
+    }
+
+    // Change sorting on column. First time it sets asc sorting on the column,
+    // next time it changes sort order.
+    $scope.changeSorting = function(column) {
+      var sort = $scope.sort
+      if (sort.column == column) {
+        sort.descending = !sort.descending
+      } else {
+        sort.column = column
+        sort.descending = false
+      }
+      $scope.reload()
+    }
+
+    $scope.selectTask = function(id) {
+      $scope.selected_task_id = id
+    }
+
+    // Reload dataset of tasks (current page) from server
+    $scope.reload = function() {
+      $scope.loading = true
+      $scope.tasks_api.query({
+        page: $scope.page,
+        sort_column: $scope.sort.column,
+        sort_order: $scope.sort.descending ? 'desc' : 'asc'
+      }, function(response) {
+        $scope.tasks = response.tasks
+        $scope.num_pages = response.pages
+        $scope.page = response.page
+        $scope.loading = false
+      })
+    }
+
+    // Open editor for editing or adding new task. Id of 0 specifies
+    // new task.
+    $scope.openEditor = function(id) {
+      $scope.edited.id = id
+
+      if (id === 0) { // New task
+        $scope.edited.description = ''
+        $scope.edited.date_start = moment.utc()
+      } else { // Existing task
+        var task = _.find($scope.tasks, function(task) {return task.id == id})
+
+        $scope.edited.description = task.description
+        $scope.edited.date_start = moment.utc(task.date_start, isoformat)
+      }
+      $('#editor-modal').modal()
+    }
+
+    $scope.setDateNow = function() {
+      $scope.edited.date_start = moment.utc()
+    }
+
+    // Save task
+    $scope.save = function() {
+      $scope.edited.saving = true
+      var ed = $scope.edited
+      var task_data = {
+        description: ed.description,
+        date_start: ed.date_start.utc().format(isoformat)
+      }
+      var on_success = function() {
+        $scope.edited.saving = false
+        $('#editor-modal').modal('hide')
+        $scope.reload()
+      }
+      if (ed.id === 0) {
+        $scope.tasks_api.create(task_data, on_success)
+      } else {
+        task_data.id = ed.id
+        $scope.task_api.save(task_data, on_success)
+      }
+    }
+
+    // Delete task
+    $scope.delete = function(id) {
+      $scope.task_api.delete({id: id}, function() {
+        $scope.loading = true
+        $scope.reload()
+      })
+    }
+
+    $scope.reload()
+  }]).
+
+  directive('datetime', function() {
+    var format = "YYYY-MM-DD HH:mm:ss"
+    var pattern = /^\s*\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\s*$/
+    return {
+      restrict: 'A',
+      require: 'ngModel',
+      link: function(scope, element, attr, ngModel) {
+        ngModel.$parsers.push(function(text) {
+          var date = moment(text, format)
+          ngModel.$setValidity('dateInvalid', date.isValid() && text.match(pattern))
+          return date
+        })
+        ngModel.$formatters.push(function(datetime_moment) {
+          return datetime_moment && datetime_moment.local().format(format)
+        })
+      }
+    };
+  }).
+
+  filter('datetime', function() {
+    return function(input) {
+      return moment.utc(input).local().format("YYYY-MM-DD HH:MM")
+    }
+  })

tasks/templates/index.html

+<!doctype html>
+{% load staticfiles %}
+<html ng-app="tasks">
+  <head>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.8/angular.min.js"></script>
+    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.8/angular-resource.min.js"></script>
+    <script src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.4.4/underscore-min.js"></script>
+    <script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.1.0/moment.min.js"></script>
+    <script src="http://code.jquery.com/jquery-1.10.1.min.js"></script>
+    <script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script>
+    <script src="{% static "tasks.js" %}"></script>
+    <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css">
+    <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap-theme.min.css">
+    <link rel="stylesheet" href="{% static 'tasks.css' %}">
+    <title>Tasks demo</title>
+  </head>
+  <body ng-controller="TasksCtrl">
+  <div class="container">
+    {% verbatim %}
+
+    <h1>Tasks
+      <span class="loading-indicator" ng-show="loading"></span>
+    </h1>
+
+    <div class="btn-group">
+      <button type="button" 
+        class="btn btn-default"
+        ng-click="openEditor(0)">
+        New</button>
+      <button type="button" 
+        class="btn btn-default"
+        ng-click="openEditor(selected_task_id)"
+        ng-disabled="! is_selected_task_in_loaded_tasks()">
+        Edit</button>
+      <button type="button"
+        class="btn btn-default"
+        ng-click="delete(selected_task_id)"
+        ng-disabled="! is_selected_task_in_loaded_tasks()">
+        Delete</button>
+    </div>
+
+    <table class="table" id="tasks-table">
+      <thead>
+        <tr class="sortable-columns">
+          <th ng-repeat="th in table_headers" ng-class="table_column_class(th[0], th[2])" ng-click="changeSorting(th[0])">{{th[1]}}</th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr ng-repeat="task in tasks" ng-dblclick="openEditor(task.id)" ng-class="{active: selected_task_id == task.id}" ng-click="selectTask(task.id)">
+          <td>{{ task.id }}</td>
+          <td class="description-cell">{{ task.description }}</td>
+          <td>{{ task.date_start|datetime }}</td>
+        </tr>
+      </tbody>
+    </table>
+
+    <ul class="pagination">
+      <li ng-click="selectPageRelative(-1)"><a href="#">&laquo;</a></li>
+      <li
+        ng-repeat="p in pages()" 
+        ng-class="{active: p == page}"
+        ng-click="selectPage(p)">
+          <a href="#">{{ p }}</a>
+      </li>
+      <li ng-click="selectPageRelative(1)"><a href="#">&raquo;</a></li>
+    </ul>
+
+    <div class="modal fade" id="editor-modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
+      <div class="modal-dialog">
+        <div class="modal-content">
+          <div class="modal-header">
+            <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
+            <h4 class="modal-title">
+              <span ng-show="edited.id">Edit task #{{ edited.id }}</span>
+              <span ng-hide="edited.id">New task</span>
+            </h4>
+          </div>
+          <div class="modal-body">
+
+            <form name="form">
+              <div class="form-group" ng-class="{'has-error': form.description.$invalid}">
+                <label for="form-description" class="control-label">Description</label>
+                <textarea class="form-control" rows="3" ng-model="edited.description" required name="description" id="form-description" ng-disable="edited.saving"></textarea>
+              </div>
+              <div class="form-group" ng-class="{'has-error': form.date_start.$invalid}">
+                <label for="form-datestart" class="control-label">Start date</label>
+                <div class="input-group">
+                  <input name="date_start" ng-model="edited.date_start" datetime required id="form-datestart" class="form-control" ng-disable="edited.saving">
+                  <span class="input-group-btn">
+                    <button class="btn btn-default" type="button" ng-click="setDateNow()" title="Reset date and time to current date and time">Now</button>
+                  </span>
+                </div>
+              </div>
+            </form>
+
+          </div>
+          <div class="modal-footer">
+            <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
+            <button type="button" class="btn btn-primary" ng-click="save()" ng-disabled="form.$invalid">Save changes</button>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    {% endverbatim %}
+  </div>
+  </body>
+</html>
+
+import json
+import math
+import itertools
+import datetime
+
+from django.test import TestCase
+from django.test.client import Client
+from django.conf import settings
+
+from models import Task
+
+class TasksTest(TestCase):
+    TASKS_IN_INITIAL_DATA = 52
+    SORT_VARIANTS = itertools.product(['id', 'description', 'date_start'], [True, False])
+
+    def test_get_tasks(self):
+        c = Client()
+
+        for sort_column, sort_reverse in self.SORT_VARIANTS:            
+            num_pages = int(math.ceil(float(self.TASKS_IN_INITIAL_DATA) / settings.TASKS_PER_PAGE))
+            all_tasks = []
+
+            # Read all of the pages sequentially, adding each page to all_tasks
+            for page in range(1, num_pages + 1):
+                r = c.get('/tasks', {
+                    'page': page,
+                    'sort_column': sort_column,
+                    'sort_order': 'desc' if sort_reverse else 'asc',
+                })
+                self.assertEqual(r.status_code, 200)
+                data = json.loads(r.content)
+
+                self.assertEquals(data['pages'], num_pages)
+                self.assertEquals(data['page'], page)
+                self.assertEquals(len(data['tasks']), settings.TASKS_PER_PAGE if page < num_pages 
+                    else self.TASKS_IN_INITIAL_DATA % settings.TASKS_PER_PAGE)
+
+                all_tasks += data['tasks']
+
+            # Check that got all of records and each occured once
+            all_ids = set(task['id'] for task in all_tasks)
+            self.assertEquals(len(all_ids), self.TASKS_IN_INITIAL_DATA)
+            self.assertEquals(len(all_tasks), self.TASKS_IN_INITIAL_DATA)
+
+            # Check for correct sorting
+            sorted_tasks = sorted(all_tasks, key=lambda t: t[sort_column], reverse=sort_reverse)
+            self.assertEquals(all_tasks, sorted_tasks)
+
+    def test_invalid_sort(self):
+        c = Client()
+        r = c.get('/tasks', {'sort_column': 'blah'})
+        self.assertEquals(r.status_code, 400)
+        r = c.get('/tasks', {'sort_column': 'id', 'sort_order': 'blah'})
+        self.assertEquals(r.status_code, 400)
+
+    def test_put_task(self):
+        tasks_before = list(Task.objects.all())
+
+        c = Client()
+        r = c.put('/tasks', json.dumps({
+            'description': 'Test Description Of Added Task',
+            'date_start': '2013-09-12T17:06:55'
+        }))
+        self.assertEquals(r.status_code, 200)
+
+        tasks_after = list(Task.objects.all())
+        diff = set(tasks_after) - set(tasks_before)
+        self.assertEquals(len(diff), 1)
+
+        added_task = diff.pop()
+        self.assertEquals(added_task.description, 'Test Description Of Added Task')
+        self.assertEquals(added_task.date_start, datetime.datetime(2013, 9, 12, 17, 6, 55))
+
+    def test_put_task_invalid_description(self):
+        r = Client().put('/tasks', json.dumps({
+            'description': '',
+            'date_start': '2013-09-12T17:24:12',
+            }), content_type='text/json')
+        self.assertEquals(r.status_code, 400)
+
+    def test_put_task_spacesonly_description(self):
+        r = Client().put('/tasks', json.dumps({
+            'description': '     ',
+            'date_start': '2013-09-12T17:24:12',
+            }), content_type='text/json')
+        self.assertEquals(r.status_code, 400)
+
+    def test_put_task_invalid_date(self):
+        r = Client().put('/tasks', json.dumps({
+            'description': 'Test descr',
+            'date_start': 'sdcjsldkjcls',
+            }), content_type='text/json')
+        self.assertEquals(r.status_code, 400)
+
+    def test_put_task_no_date(self):
+        r = Client().put('/tasks', json.dumps({
+            'description': 'Test descr',
+            }), content_type='text/json')
+        self.assertEquals(r.status_code, 400)
+
+    def test_post_task(self):
+        r = Client().post('/tasks/3', json.dumps({
+            'description': 'New description 111',
+            'date_start': '2013-09-13T17:39:32',
+            }), content_type='text/json')
+        self.assertEquals(r.status_code, 200)
+        task = Task.objects.get(pk=3)
+        self.assertEquals(task.description, "New description 111")
+        self.assertEquals(task.date_start, datetime.datetime(2013, 9, 13, 17, 39, 32))
+
+    def test_post_task_invalid(self):
+        r = Client().post('/tasks/3', json.dumps({
+            'description': '',
+            'date_start': '2013-09-13T17:39:32',
+            }), content_type='text/json')
+        self.assertEquals(r.status_code, 400)
+
+    def test_post_task_404(self):
+        r = Client().post('/tasks/9999', json.dumps({
+            'description': '',
+            'date_start': '2013-08-13T17:39:37',
+            }), content_type='text/json')
+        self.assertEquals(r.status_code, 404)
+
+    def test_delete_task(self):
+        r = Client().delete('/tasks/12')
+        self.assertEquals(r.status_code, 200)
+
+        ids = set(task.id for task in Task.objects.all())
+        expected_ids = set(range(1, self.TASKS_IN_INITIAL_DATA + 1)) - set([12])
+        self.assertEquals(ids, expected_ids)
+
+    def test_detele_task_404(self):
+        r = Client().delete('/tasks/2873')
+        self.assertEquals(r.status_code, 404)
+
+    def test_invalid_methods(self):
+        r = Client().delete('/tasks')
+        self.assertEquals(r.status_code, 405)
+        r = Client().post('/tasks')
+        self.assertEquals(r.status_code, 405)
+        r = Client().get('/tasks/23')
+        self.assertEquals(r.status_code, 405)
+        r = Client().put('/tasks/43')
+        self.assertEquals(r.status_code, 405)
+
+    def test_index_page(self):
+        r = Client().get('/')
+        self.assertEquals(r.status_code, 200)
+import json
+
+from django.http import HttpResponse, HttpResponseNotAllowed, HttpResponseBadRequest
+from django.core.paginator import Paginator, EmptyPage
+from django.conf import settings
+from django.shortcuts import get_object_or_404
+from django.views.decorators.http import require_http_methods
+
+from models import Task, ValidationError
+from decorators import json_request_body
+
+def _validate_and_save_task_from_api(task, data):
+    try:
+        task.update_from_api(data)
+    except ValidationError as e:
+        return HttpResponseBadRequest(str(e))
+
+    task.save()
+    return HttpResponse()
+
+def _tasks_get(request):
+    page = int(request.GET.get('page', 1))
+    sort_column = request.GET.get('sort_column', 'id')
+    sort_order = request.GET.get('sort_order', 'asc')
+
+    if (sort_column not in ['id', 'description', 'date_start'] or
+            sort_order not in ['asc', 'desc']):
+        return HttpResponseBadRequest("Invalid sort specification")
+
+    q = Task.objects.all().order_by(sort_column)
+    if sort_order == 'desc':
+        q = q.reverse()
+    paginator = Paginator(q, settings.TASKS_PER_PAGE)
+
+    try:
+        tasks = paginator.page(page)
+    except EmptyPage:
+        # If requested page with number larger than number of pages, return last page
+        page = paginator.num_pages
+        tasks = paginator.page(page)
+
+    result = {
+        'tasks': [task.to_api() for task in tasks.object_list],
+        'page': page,
+        'pages': paginator.num_pages,
+    }
+
+    return HttpResponse(json.dumps(result), content_type="application/json")
+
+@json_request_body
+def _tasks_put(request):
+    task = Task()
+    return _validate_and_save_task_from_api(task, request.data)
+
+@json_request_body
+def _task_post(request, id):
+    task = get_object_or_404(Task, pk=id)
+    return _validate_and_save_task_from_api(task, request.data)
+
+def _task_delete(request, id):
+    get_object_or_404(Task, pk=id).delete()
+    return HttpResponse()
+
+@require_http_methods(["GET", "PUT"])
+def tasks(request):
+    """
+    Tasks view: GET returns a list of tasks, paged and sorted, PUT creates a new task
+    """
+    if request.method == "GET":
+        return _tasks_get(request)
+    elif request.method == "PUT":
+        return _tasks_put(request)
+
+@require_http_methods(["POST", "DELETE"])
+def task(request, id):
+    """
+    Single task view: POST modifies task, DELETE deletes it
+    """
+    if request.method == "POST":
+        return _task_post(request, id)
+    elif request.method == "DELETE":    
+        return _task_delete(request, id)
+

test_tasks/__init__.py

Empty file added.

test_tasks/settings.py

+# Django settings for test_tasks project.
+
+DEBUG = True
+TEMPLATE_DEBUG = DEBUG
+
+ADMINS = (
+    ('kolen', 'incredible.angst@gmail.com'),
+)
+
+MANAGERS = ADMINS
+
+DATABASES = {
+    'default': {
+        'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
+        'NAME': 'db.sqlite3',                      # Or path to database file if using sqlite3.
+        # The following settings are not used with sqlite3:
+        'USER': '',
+        'PASSWORD': '',
+        'HOST': '',                      # Empty for localhost through domain sockets or '127.0.0.1' for localhost through TCP.
+        'PORT': '',                      # Set to empty string for default.
+    }
+}
+
+# Hosts/domain names that are valid for this site; required if DEBUG is False
+# See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts
+ALLOWED_HOSTS = []
+
+# 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.
+# In a Windows environment this must be set to your system time zone.
+TIME_ZONE = 'Europe/Moscow'
+
+# Language code for this installation. All choices can be found here:
+# http://www.i18nguy.com/unicode/language-identifiers.html
+LANGUAGE_CODE = 'ru-RU'
+
+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 = False
+
+# If you set this to False, Django will not use timezone-aware datetimes.
+USE_TZ = False
+
+# Absolute filesystem path to the directory that will hold user-uploaded files.
+# Example: "/var/www/example.com/media/"
+MEDIA_ROOT = ''
+
+# URL that handles the media served from MEDIA_ROOT. Make sure to use a
+# trailing slash.
+# Examples: "http://example.com/media/", "http://media.example.com/"
+MEDIA_URL = ''
+
+# 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: "/var/www/example.com/static/"
+STATIC_ROOT = ''
+
+# URL prefix for static files.
+# Example: "http://example.com/static/", "http://static.example.com/"
+STATIC_URL = '/static/'
+
+# 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',
+)
+
+# Make this unique, and don't share it with anybody.
+SECRET_KEY = '6)k2dh9(080cbt$=5lo9$$&ho2+x#s31-2u)8uazm8#s!-un2%'
+
+# 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',
+    # Uncomment the next line for simple clickjacking protection:
+    # 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+)
+
+ROOT_URLCONF = 'test_tasks.urls'
+
+# Python dotted path to the WSGI application used by Django's runserver.
+WSGI_APPLICATION = 'test_tasks.wsgi.application'
+
+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.
+)
+
+INSTALLED_APPS = (
+    'tasks',
+
+    # 'django.contrib.auth',
+    'django.contrib.contenttypes',
+    # 'django.contrib.sessions',
+    'django.contrib.sites',
+    'django.contrib.messages',
+    'django.contrib.staticfiles',
+    # Uncomment the next line to enable the admin:
+    # 'django.contrib.admin',
+    # Uncomment the next line to enable admin documentation:
+    # 'django.contrib.admindocs',
+)
+
+SESSION_SERIALIZER = 'django.contrib.sessions.serializers.JSONSerializer'
+
+# 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 when DEBUG=False.
+# 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,
+    'filters': {
+        'require_debug_false': {
+            '()': 'django.utils.log.RequireDebugFalse'
+        }
+    },
+    'handlers': {
+        'mail_admins': {
+            'level': 'ERROR',
+            'filters': ['require_debug_false'],
+            'class': 'django.utils.log.AdminEmailHandler'
+        }
+    },
+    'loggers': {
+        'django.request': {
+            'handlers': ['mail_admins'],
+            'level': 'ERROR',
+            'propagate': True,
+        },
+    }
+}
+
+TASKS_PER_PAGE = 10

test_tasks/urls.py

+from django.conf.urls import patterns, include, url
+from django.views.generic import TemplateView
+
+# Uncomment the next two lines to enable the admin:
+# from django.contrib import admin
+# admin.autodiscover()
+
+urlpatterns = patterns('',
+    # Examples:
+    # url(r'^$', 'test_tasks.views.home', name='home'),
+    # url(r'^test_tasks/', include('test_tasks.foo.urls')),
+
+    # Uncomment the admin/doc line below to enable admin documentation:
+    # url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
+
+    # Uncomment the next line to enable the admin:
+    # url(r'^admin/', include(admin.site.urls)),
+
+    url(r'^tasks$', 'tasks.views.tasks'),
+    url(r'^tasks/(?P<id>\d+)$', 'tasks.views.task'),
+    url(r'^$', TemplateView.as_view(template_name="index.html")),
+)

test_tasks/wsgi.py

+"""
+WSGI config for test_tasks project.
+
+This module contains the WSGI application used by Django's development server
+and any production WSGI deployments. It should expose a module-level variable
+named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover
+this application via the ``WSGI_APPLICATION`` setting.
+
+Usually you will have the standard Django WSGI application here, but it also
+might make sense to replace the whole Django WSGI application with a custom one
+that later delegates to the Django one. For example, you could introduce WSGI
+middleware here, or combine a Django application with an application of another
+framework.
+
+"""
+import os
+
+# We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks
+# if running multiple sites in the same mod_wsgi process. To fix this, use
+# mod_wsgi daemon mode with each site in its own daemon process, or use
+# os.environ["DJANGO_SETTINGS_MODULE"] = "test_tasks.settings"
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_tasks.settings")
+
+# This application object is used by any WSGI server configured to use this
+# file. This includes Django's development server, if the WSGI_APPLICATION
+# setting points here.
+from django.core.wsgi import get_wsgi_application
+application = get_wsgi_application()
+
+# Apply WSGI middleware here.
+# from helloworld.wsgi import HelloWorldApplication
+# application = HelloWorldApplication(application)