aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFilipp Lepalaan <f@0x00.co>2013-01-31 20:08:20 +0200
committerFilipp Lepalaan <f@0x00.co>2013-01-31 20:08:20 +0200
commitc0121b9b7f5041f6434a2e2dd24d3c68ed84b582 (patch)
treee3e8d0a05c45dabcc2a0399f902c6a59c92a9f4f
parente613dc2b8b17e6f58a9666bee03578bb7ffc4067 (diff)
downloadopus-c0121b9b7f5041f6434a2e2dd24d3c68ed84b582.tar.gz
opus-c0121b9b7f5041f6434a2e2dd24d3c68ed84b582.tar.bz2
opus-c0121b9b7f5041f6434a2e2dd24d3c68ed84b582.zip
Initial commit
-rw-r--r--.gitignore1
-rw-r--r--README.md5
-rwxr-xr-xmanage.py10
-rw-r--r--notes/__init__.py0
-rw-r--r--notes/models.py63
-rw-r--r--notes/templates/edit.html31
-rw-r--r--notes/templates/index.html27
-rw-r--r--notes/templates/view.html15
-rw-r--r--notes/tests.py16
-rw-r--r--notes/views.py51
-rw-r--r--opus/__init__.py0
-rw-r--r--opus/settings.py153
-rw-r--r--opus/templates/blank.html3
-rw-r--r--opus/templates/default.html69
-rw-r--r--opus/urls.py32
-rw-r--r--opus/wsgi.py28
-rw-r--r--timer/__init__.py0
-rw-r--r--timer/models.py44
-rw-r--r--timer/static/js/backbone-min.js42
-rw-r--r--timer/static/js/underscore-min.js1
-rw-r--r--timer/templates/alarm.html68
-rw-r--r--timer/templates/default.html62
-rw-r--r--timer/templates/event_detail.html13
-rw-r--r--timer/templates/event_grid.html30
-rw-r--r--timer/templates/event_list.html5
-rw-r--r--timer/templates/label_list.html3
-rw-r--r--timer/templates/labels.html31
-rw-r--r--timer/templates/timer.html59
-rw-r--r--timer/tests.py16
-rw-r--r--timer/views.py102
30 files changed, 979 insertions, 1 deletions
diff --git a/.gitignore b/.gitignore
index d9437c3..da292ce 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,4 @@
*.pot
*.pyc
local_settings.py
+*.db
diff --git a/README.md b/README.md
index 7db1e06..9ba8500 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,7 @@
opus
====
-Tools that every office needs \ No newline at end of file
+Tools that every office needs:
+
+- Shared notes
+- Time tracking
diff --git a/manage.py b/manage.py
new file mode 100755
index 0000000..9391588
--- /dev/null
+++ b/manage.py
@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+import os
+import sys
+
+if __name__ == "__main__":
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "opus.settings")
+
+ from django.core.management import execute_from_command_line
+
+ execute_from_command_line(sys.argv)
diff --git a/notes/__init__.py b/notes/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/notes/__init__.py
diff --git a/notes/models.py b/notes/models.py
new file mode 100644
index 0000000..2cd5ca6
--- /dev/null
+++ b/notes/models.py
@@ -0,0 +1,63 @@
+import re
+from datetime import datetime
+from django.db import models
+from django.contrib.auth.models import User
+from django.db.models.signals import post_save
+from django.dispatch import receiver
+
+class Tag(models.Model):
+ title = models.CharField(max_length=128)
+
+ class Meta:
+ ordering = ['title']
+
+class Note(models.Model):
+ user = models.ForeignKey(User)
+ shared = models.BooleanField(default=True)
+ title = models.CharField(max_length=140, null=True)
+ tags = models.ManyToManyField(Tag, null=True, blank=True)
+
+ def get_date(self):
+ return self.version_set.all()[0].created_at
+
+ def get_user(self):
+ pass
+
+ def content(self):
+ try:
+ return self.version_set.latest().content
+ except Version.DoesNotExist:
+ return ''
+
+ def updated_at(self):
+ return self.version_set.latest().created_at
+
+ def get_absolute_url(self):
+ return '/notes/%d/' % self.pk
+
+ class Meta:
+ ordering = ['-id']
+
+class Version(models.Model):
+ note = models.ForeignKey(Note)
+ user = models.ForeignKey(User)
+ content = models.TextField()
+ created_at = models.DateTimeField(default=datetime.now())
+
+ class Meta:
+ get_latest_by = 'created_at'
+
+class Attachment(models.Model):
+ content = models.FileField(upload_to='uploads')
+ note = models.ForeignKey(Note)
+
+@receiver(post_save, sender=Version)
+def version_saved(sender, instance, created, **kwargs):
+
+ tags = re.findall('#(\w+)', instance.content)
+
+ for t in tags:
+ tag = Tag.objects.get_or_create(title=t)[0]
+ instance.note.tags.add(tag)
+
+ instance.note.save()
diff --git a/notes/templates/edit.html b/notes/templates/edit.html
new file mode 100644
index 0000000..6d2ec1e
--- /dev/null
+++ b/notes/templates/edit.html
@@ -0,0 +1,31 @@
+{% extends request.is_ajax|yesno:"blank.html,index.html" %}
+
+{% block content %}
+<div data-role="page">
+ <div data-role="header">
+ <a href="/notes/" data-icon="arrow-l">Notes</a>
+ <h1>New Note</h1>
+ <a href="/notes/" id="done">Done</a>
+ </div>
+ <div data-role="content">
+ <form action="" method="post" id="noteForm" enctype="multipart/form-data">
+ {% csrf_token %}
+ {{ form }}
+ <button type="submit" data-theme="b">Save</button>
+ {% if form.is_bound %}
+ <a href="delete/" data-role="button">Delete</a>
+ {% endif %}
+ </form>
+ </div>
+</div>
+
+<script type="text/javascript">
+ $(function(){
+ $('#id_title').focus();
+ });
+ $('#done').click(function(e){
+ e.preventDefault();
+ $('#noteForm').submit();
+ });
+</script>
+{% endblock content %}
diff --git a/notes/templates/index.html b/notes/templates/index.html
new file mode 100644
index 0000000..c996511
--- /dev/null
+++ b/notes/templates/index.html
@@ -0,0 +1,27 @@
+{% extends request.is_ajax|yesno:"blank.html,default.html" %}
+
+{% block content %}
+<div data-role="page">
+ <div data-role="header" data-position="fixed">
+ <a href="#tagPopup" data-icon="gear" data-rel="popup">Tags</a>
+ <h1>Notes</h1>
+ <a href="/notes/new/" class="ui-btn-right">New</a>
+ </div>
+ <div data-role="content">
+ <ul data-role="listview" data-filter="true">
+ {% for n in notes %}
+ <li><a href="{{ n.get_absolute_url }}">{{ n.title }}<p class="ui-li-aside ui-li-desc">{{ n.user.username }} @ {{ n.updated_at|date:"SHORT_DATE_FORMAT" }}</p></a></li>
+ {% endfor %}
+ </ul>
+
+ <div data-role="popup" id="tagPopup">
+ <ul data-role="listview" data-inset="true">
+ {% for t in tags %}
+ <li><a href="/notes/tag/{{ t.pk }}/">{{ t.title }} <span class="ui-li-count">{{ t.note_set.count }}</span></a></li>
+ {% endfor %}
+ </ul>
+ </div>
+ </div>
+</div>
+
+{% endblock content %}
diff --git a/notes/templates/view.html b/notes/templates/view.html
new file mode 100644
index 0000000..a6d3473
--- /dev/null
+++ b/notes/templates/view.html
@@ -0,0 +1,15 @@
+{% extends request.is_ajax|yesno:"blank.html,index.html" %}
+{% load markup %}
+
+{% block content %}
+<div data-role="page">
+ <div data-role="header">
+ <a href="/notes/" data-icon="arrow-l">Notes</a>
+ <h1>{{ note.title }}</h1>
+ <a href="{{ note.get_absolute_url }}edit/">Edit</a>
+ </div>
+ <div data-role="content">
+ <p>{{ version.content|restructuredtext }}</p>
+ </div>
+</div>
+{% endblock content %}
diff --git a/notes/tests.py b/notes/tests.py
new file mode 100644
index 0000000..501deb7
--- /dev/null
+++ b/notes/tests.py
@@ -0,0 +1,16 @@
+"""
+This file demonstrates writing tests using the unittest module. These will pass
+when you run "manage.py test".
+
+Replace this with more appropriate tests for your application.
+"""
+
+from django.test import TestCase
+
+
+class SimpleTest(TestCase):
+ def test_basic_addition(self):
+ """
+ Tests that 1 + 1 always equals 2.
+ """
+ self.assertEqual(1 + 1, 2)
diff --git a/notes/views.py b/notes/views.py
new file mode 100644
index 0000000..386b0c0
--- /dev/null
+++ b/notes/views.py
@@ -0,0 +1,51 @@
+from django import forms
+from django.shortcuts import render, redirect
+from notes.models import Note, Attachment, Tag, Version
+
+class NoteForm(forms.Form):
+ title = forms.CharField()
+ content = forms.CharField(widget=forms.Textarea(attrs={'rows': 20,
+ 'style': 'width:100%;height:100%'}))
+ shared = forms.BooleanField()
+ attachment = forms.FileField(required=False)
+
+def edit(request, note_id=None):
+
+ note = Note(user_id=1)
+
+ if note_id:
+ note = Note.objects.get(pk=note_id)
+
+ if request.method == 'POST':
+ form = NoteForm(request.POST, request.FILES)
+
+ if not form.is_valid():
+ return render(request, 'edit.html', {'form': form})
+
+ note.title = form.cleaned_data.get('title')
+ note.save()
+
+ version = Version(note=note, user_id=1)
+ version.content = form.cleaned_data.get('content')
+ version.shared = form.cleaned_data.get('shared')
+ version.save()
+
+ return render(request, 'view.html', {'note': note, 'version': version})
+
+ form = NoteForm(initial={'content': note.content, 'shared': note.shared})
+
+ return render(request, 'edit.html', {'form': form})
+
+def index(request, tag_id=None):
+ notes = Note.objects.filter(user_id=1)
+
+ if tag_id:
+ notes = notes.filter(tags__pk=tag_id)
+
+ tags = Tag.objects.distinct()
+ return render(request, 'index.html', {'notes': notes, 'tags': tags})
+
+def view(request, note_id):
+ note = Note.objects.get(pk=note_id)
+ version = note.version_set.latest()
+ return render(request, 'view.html', {'note': note, 'version': version})
diff --git a/opus/__init__.py b/opus/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/opus/__init__.py
diff --git a/opus/settings.py b/opus/settings.py
new file mode 100644
index 0000000..2567e1b
--- /dev/null
+++ b/opus/settings.py
@@ -0,0 +1,153 @@
+# Django settings for opus project.
+
+DEBUG = True
+TEMPLATE_DEBUG = DEBUG
+
+ADMINS = (
+ # ('Your Name', 'your_email@example.com'),
+)
+
+MANAGERS = ADMINS
+
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
+ 'NAME': 'opus.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.
+# In a Windows environment this must be set to your system time zone.
+TIME_ZONE = 'America/Chicago'
+
+# Language code for this installation. All choices can be found here:
+# http://www.i18nguy.com/unicode/language-identifiers.html
+LANGUAGE_CODE = 'en-us'
+
+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
+
+# 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: "/home/media/media.lawrence.com/media/"
+MEDIA_ROOT = ''
+
+# 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 = ''
+
+# 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 = ''
+
+# URL prefix for static files.
+# Example: "http://media.lawrence.com/static/"
+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 = 'p3bkcgfs4bv5+0moy8^w^njqxqc#fn9(&amp;s^a7_=2r&amp;f750@0c#'
+
+# 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 = 'opus.urls'
+
+# Python dotted path to the WSGI application used by Django's runserver.
+WSGI_APPLICATION = 'opus.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 = (
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ #'django.contrib.sites',
+ 'django.contrib.messages',
+ 'django.contrib.staticfiles',
+ 'django.contrib.markup',
+ 'timer', 'notes'
+ # Uncomment the next line to enable the admin:
+ # 'django.contrib.admin',
+ # Uncomment the next line to enable admin documentation:
+ # 'django.contrib.admindocs',
+)
+
+# 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,
+ },
+ }
+}
diff --git a/opus/templates/blank.html b/opus/templates/blank.html
new file mode 100644
index 0000000..e7f8a7b
--- /dev/null
+++ b/opus/templates/blank.html
@@ -0,0 +1,3 @@
+{% block content %}
+
+{% endblock content %}
diff --git a/opus/templates/default.html b/opus/templates/default.html
new file mode 100644
index 0000000..c970b6b
--- /dev/null
+++ b/opus/templates/default.html
@@ -0,0 +1,69 @@
+{% load staticfiles %}
+<!DOCTYPE html>
+<html>
+<head>
+ <title>opus v0.01</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <link rel="stylesheet" href="http://code.jquery.com/mobile/1.2.0/jquery.mobile-1.2.0.min.css" />
+ <script src="http://code.jquery.com/jquery-1.8.2.min.js"></script>
+ <script src="http://code.jquery.com/mobile/1.2.0/jquery.mobile-1.2.0.min.js"></script>
+ <script type="text/javascript">
+ function getDate(field) {
+ // 2013-01-30 08:36:48
+ var splitRex = /^(\d{4})\-(\d{2})\-(\d{2})\s(\d{2}):(\d{2}):(\d{2})$/;
+ var s = $(field).val().match(splitRex);
+ return new Date(s[1], parseInt(s[2])-1, s[3], s[4], s[5], s[6], 0);
+ }
+
+ function toDateTime(date) {
+ function pad(n){return n<10 ? '0'+n : n}
+ return date.getFullYear()+'-'
+ +pad(date.getMonth()+1)+'-'
+ +pad(date.getDate())+' '
+ +pad(date.getHours())+':'
+ +pad(date.getMinutes())+':'
+ +pad(date.getSeconds());
+ }
+
+ $(function(){
+ $('#duration').blur(function(){
+ if($('#id_started_at').val() == '') return;
+
+ console.log('Calculate finished_at!');
+
+ started_at = getDate('#id_started_at');
+ var d = $(this).val().split(':');
+ // H:M:S
+ var seconds = 0;
+ if (d[0]) seconds += parseInt(d[0]*3600);
+ if (d[1]) seconds += parseInt(d[1]*60);
+ if (d[2]) seconds += parseInt(d[2]);
+ finished_at = new Date(started_at.getTime() + (seconds*1000));
+ $('#id_finished_at').val(toDateTime(finished_at));
+
+ $('#id_duration').val(seconds);
+
+ });
+
+ $('#id_finished_at').blur(function(){
+ if($('#id_duration').val() == '') return;
+
+ console.log('Calculate duration...');
+ finished_at = getDate('#id_finished_at');
+ started_at = getDate('#id_started_at');
+ seconds = (finished_at - started_at)/1000;
+ $('#id_duration').val(seconds);
+
+ hours = seconds/3600;
+
+ $('#duration').val(hours);
+ });
+ });
+ </script>
+</head>
+ <body id="app">
+ {% block content %}
+
+ {% endblock content %}
+ </body>
+</html>
diff --git a/opus/urls.py b/opus/urls.py
new file mode 100644
index 0000000..75e20ee
--- /dev/null
+++ b/opus/urls.py
@@ -0,0 +1,32 @@
+from django.conf.urls import patterns, include, url
+
+# Uncomment the next two lines to enable the admin:
+# from django.contrib import admin
+# admin.autodiscover()
+
+urlpatterns = patterns('',
+ # Examples:
+ # url(r'^$', 'opus2.views.home', name='home'),
+ # url(r'^opus2/', include('opus2.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'^timer/$', 'timer.views.labels'),
+ url(r'^timer/events/(\d+)/$', 'timer.views.view_event'),
+ url(r'^timer/events/(\d+)/delete/$', 'timer.views.delete_event'),
+ url(r'^timer/label/(\d+)/events/$', 'timer.views.events'),
+ url(r'^timer/labels/$', 'timer.views.labels'),
+ url(r'^timer/labels/new/$', 'timer.views.edit_label'),
+
+ url(r'^alarm/$', 'timer.views.alarm'),
+
+ url(r'^notes/$', 'notes.views.index'),
+ url(r'^notes/tag/(\d+)/$', 'notes.views.index'),
+
+ url(r'^notes/(\d+)/$', 'notes.views.view'),
+ url(r'^notes/new/$', 'notes.views.edit'),
+ url(r'^notes/(\d+)/edit/$', 'notes.views.edit'),
+)
diff --git a/opus/wsgi.py b/opus/wsgi.py
new file mode 100644
index 0000000..f323b3a
--- /dev/null
+++ b/opus/wsgi.py
@@ -0,0 +1,28 @@
+"""
+WSGI config for opus2 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
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "opus2.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)
diff --git a/timer/__init__.py b/timer/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/timer/__init__.py
diff --git a/timer/models.py b/timer/models.py
new file mode 100644
index 0000000..1052eca
--- /dev/null
+++ b/timer/models.py
@@ -0,0 +1,44 @@
+from datetime import datetime
+
+from django.db import models
+from django.contrib.auth.models import User
+
+class Label(models.Model):
+ user = models.ForeignKey(User, editable=False)
+ title = models.CharField(max_length=128)
+ color = models.CharField(max_length=6, default="red")
+
+ def get_absolute_url(self):
+ return "/timer/label/%d/events/" % self.pk
+
+ def __unicode__(self):
+ return self.title
+
+ class Meta:
+ ordering = ['-id']
+
+class Event(models.Model):
+ user = models.ForeignKey(User, editable=False)
+ started_at = models.DateTimeField(default=datetime.now())
+ duration = models.IntegerField(null=True, blank=True)
+ finished_at = models.DateTimeField(null=True, blank=True) # in seconds
+ labels = models.ManyToManyField(Label, null=True, blank=True)
+ notes = models.TextField(null=True, blank=True)
+
+ def hours(self):
+ return self.duration/3600
+
+ def title(self):
+ if self.notes:
+ return '%s: %s' % (self.notes, self.duration())
+
+ return self.started_at
+
+ def as_json(self):
+ return {'started_at': int(self.started_at.strftime('%s'))*1000}
+
+ def get_absolute_url(self):
+ return "/timer/events/%d/" % self.pk
+
+ class Meta:
+ ordering = ['-started_at']
diff --git a/timer/static/js/backbone-min.js b/timer/static/js/backbone-min.js
new file mode 100644
index 0000000..d4b0314
--- /dev/null
+++ b/timer/static/js/backbone-min.js
@@ -0,0 +1,42 @@
+// Backbone.js 0.9.10
+
+// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
+// Backbone may be freely distributed under the MIT license.
+// For all details and documentation:
+// http://backbonejs.org
+(function(){var n=this,B=n.Backbone,h=[],C=h.push,u=h.slice,D=h.splice,g;g="undefined"!==typeof exports?exports:n.Backbone={};g.VERSION="0.9.10";var f=n._;!f&&"undefined"!==typeof require&&(f=require("underscore"));g.$=n.jQuery||n.Zepto||n.ender;g.noConflict=function(){n.Backbone=B;return this};g.emulateHTTP=!1;g.emulateJSON=!1;var v=/\s+/,q=function(a,b,c,d){if(!c)return!0;if("object"===typeof c)for(var e in c)a[b].apply(a,[e,c[e]].concat(d));else if(v.test(c)){c=c.split(v);e=0;for(var f=c.length;e<
+f;e++)a[b].apply(a,[c[e]].concat(d))}else return!0},w=function(a,b){var c,d=-1,e=a.length;switch(b.length){case 0:for(;++d<e;)(c=a[d]).callback.call(c.ctx);break;case 1:for(;++d<e;)(c=a[d]).callback.call(c.ctx,b[0]);break;case 2:for(;++d<e;)(c=a[d]).callback.call(c.ctx,b[0],b[1]);break;case 3:for(;++d<e;)(c=a[d]).callback.call(c.ctx,b[0],b[1],b[2]);break;default:for(;++d<e;)(c=a[d]).callback.apply(c.ctx,b)}},h=g.Events={on:function(a,b,c){if(!q(this,"on",a,[b,c])||!b)return this;this._events||(this._events=
+{});(this._events[a]||(this._events[a]=[])).push({callback:b,context:c,ctx:c||this});return this},once:function(a,b,c){if(!q(this,"once",a,[b,c])||!b)return this;var d=this,e=f.once(function(){d.off(a,e);b.apply(this,arguments)});e._callback=b;this.on(a,e,c);return this},off:function(a,b,c){var d,e,t,g,j,l,k,h;if(!this._events||!q(this,"off",a,[b,c]))return this;if(!a&&!b&&!c)return this._events={},this;g=a?[a]:f.keys(this._events);j=0;for(l=g.length;j<l;j++)if(a=g[j],d=this._events[a]){t=[];if(b||
+c){k=0;for(h=d.length;k<h;k++)e=d[k],(b&&b!==e.callback&&b!==e.callback._callback||c&&c!==e.context)&&t.push(e)}this._events[a]=t}return this},trigger:function(a){if(!this._events)return this;var b=u.call(arguments,1);if(!q(this,"trigger",a,b))return this;var c=this._events[a],d=this._events.all;c&&w(c,b);d&&w(d,arguments);return this},listenTo:function(a,b,c){var d=this._listeners||(this._listeners={}),e=a._listenerId||(a._listenerId=f.uniqueId("l"));d[e]=a;a.on(b,"object"===typeof b?this:c,this);
+return this},stopListening:function(a,b,c){var d=this._listeners;if(d){if(a)a.off(b,"object"===typeof b?this:c,this),!b&&!c&&delete d[a._listenerId];else{"object"===typeof b&&(c=this);for(var e in d)d[e].off(b,c,this);this._listeners={}}return this}}};h.bind=h.on;h.unbind=h.off;f.extend(g,h);var r=g.Model=function(a,b){var c,d=a||{};this.cid=f.uniqueId("c");this.attributes={};b&&b.collection&&(this.collection=b.collection);b&&b.parse&&(d=this.parse(d,b)||{});if(c=f.result(this,"defaults"))d=f.defaults({},
+d,c);this.set(d,b);this.changed={};this.initialize.apply(this,arguments)};f.extend(r.prototype,h,{changed:null,idAttribute:"id",initialize:function(){},toJSON:function(){return f.clone(this.attributes)},sync:function(){return g.sync.apply(this,arguments)},get:function(a){return this.attributes[a]},escape:function(a){return f.escape(this.get(a))},has:function(a){return null!=this.get(a)},set:function(a,b,c){var d,e,g,p,j,l,k;if(null==a)return this;"object"===typeof a?(e=a,c=b):(e={})[a]=b;c||(c={});
+if(!this._validate(e,c))return!1;g=c.unset;p=c.silent;a=[];j=this._changing;this._changing=!0;j||(this._previousAttributes=f.clone(this.attributes),this.changed={});k=this.attributes;l=this._previousAttributes;this.idAttribute in e&&(this.id=e[this.idAttribute]);for(d in e)b=e[d],f.isEqual(k[d],b)||a.push(d),f.isEqual(l[d],b)?delete this.changed[d]:this.changed[d]=b,g?delete k[d]:k[d]=b;if(!p){a.length&&(this._pending=!0);b=0;for(d=a.length;b<d;b++)this.trigger("change:"+a[b],this,k[a[b]],c)}if(j)return this;
+if(!p)for(;this._pending;)this._pending=!1,this.trigger("change",this,c);this._changing=this._pending=!1;return this},unset:function(a,b){return this.set(a,void 0,f.extend({},b,{unset:!0}))},clear:function(a){var b={},c;for(c in this.attributes)b[c]=void 0;return this.set(b,f.extend({},a,{unset:!0}))},hasChanged:function(a){return null==a?!f.isEmpty(this.changed):f.has(this.changed,a)},changedAttributes:function(a){if(!a)return this.hasChanged()?f.clone(this.changed):!1;var b,c=!1,d=this._changing?
+this._previousAttributes:this.attributes,e;for(e in a)if(!f.isEqual(d[e],b=a[e]))(c||(c={}))[e]=b;return c},previous:function(a){return null==a||!this._previousAttributes?null:this._previousAttributes[a]},previousAttributes:function(){return f.clone(this._previousAttributes)},fetch:function(a){a=a?f.clone(a):{};void 0===a.parse&&(a.parse=!0);var b=a.success;a.success=function(a,d,e){if(!a.set(a.parse(d,e),e))return!1;b&&b(a,d,e)};return this.sync("read",this,a)},save:function(a,b,c){var d,e,g=this.attributes;
+null==a||"object"===typeof a?(d=a,c=b):(d={})[a]=b;if(d&&(!c||!c.wait)&&!this.set(d,c))return!1;c=f.extend({validate:!0},c);if(!this._validate(d,c))return!1;d&&c.wait&&(this.attributes=f.extend({},g,d));void 0===c.parse&&(c.parse=!0);e=c.success;c.success=function(a,b,c){a.attributes=g;var k=a.parse(b,c);c.wait&&(k=f.extend(d||{},k));if(f.isObject(k)&&!a.set(k,c))return!1;e&&e(a,b,c)};a=this.isNew()?"create":c.patch?"patch":"update";"patch"===a&&(c.attrs=d);a=this.sync(a,this,c);d&&c.wait&&(this.attributes=
+g);return a},destroy:function(a){a=a?f.clone(a):{};var b=this,c=a.success,d=function(){b.trigger("destroy",b,b.collection,a)};a.success=function(a,b,e){(e.wait||a.isNew())&&d();c&&c(a,b,e)};if(this.isNew())return a.success(this,null,a),!1;var e=this.sync("delete",this,a);a.wait||d();return e},url:function(){var a=f.result(this,"urlRoot")||f.result(this.collection,"url")||x();return this.isNew()?a:a+("/"===a.charAt(a.length-1)?"":"/")+encodeURIComponent(this.id)},parse:function(a){return a},clone:function(){return new this.constructor(this.attributes)},
+isNew:function(){return null==this.id},isValid:function(a){return!this.validate||!this.validate(this.attributes,a)},_validate:function(a,b){if(!b.validate||!this.validate)return!0;a=f.extend({},this.attributes,a);var c=this.validationError=this.validate(a,b)||null;if(!c)return!0;this.trigger("invalid",this,c,b||{});return!1}});var s=g.Collection=function(a,b){b||(b={});b.model&&(this.model=b.model);void 0!==b.comparator&&(this.comparator=b.comparator);this.models=[];this._reset();this.initialize.apply(this,
+arguments);a&&this.reset(a,f.extend({silent:!0},b))};f.extend(s.prototype,h,{model:r,initialize:function(){},toJSON:function(a){return this.map(function(b){return b.toJSON(a)})},sync:function(){return g.sync.apply(this,arguments)},add:function(a,b){a=f.isArray(a)?a.slice():[a];b||(b={});var c,d,e,g,p,j,l,k,h,m;l=[];k=b.at;h=this.comparator&&null==k&&!1!=b.sort;m=f.isString(this.comparator)?this.comparator:null;c=0;for(d=a.length;c<d;c++)(e=this._prepareModel(g=a[c],b))?(p=this.get(e))?b.merge&&(p.set(g===
+e?e.attributes:g,b),h&&(!j&&p.hasChanged(m))&&(j=!0)):(l.push(e),e.on("all",this._onModelEvent,this),this._byId[e.cid]=e,null!=e.id&&(this._byId[e.id]=e)):this.trigger("invalid",this,g,b);l.length&&(h&&(j=!0),this.length+=l.length,null!=k?D.apply(this.models,[k,0].concat(l)):C.apply(this.models,l));j&&this.sort({silent:!0});if(b.silent)return this;c=0;for(d=l.length;c<d;c++)(e=l[c]).trigger("add",e,this,b);j&&this.trigger("sort",this,b);return this},remove:function(a,b){a=f.isArray(a)?a.slice():[a];
+b||(b={});var c,d,e,g;c=0;for(d=a.length;c<d;c++)if(g=this.get(a[c]))delete this._byId[g.id],delete this._byId[g.cid],e=this.indexOf(g),this.models.splice(e,1),this.length--,b.silent||(b.index=e,g.trigger("remove",g,this,b)),this._removeReference(g);return this},push:function(a,b){a=this._prepareModel(a,b);this.add(a,f.extend({at:this.length},b));return a},pop:function(a){var b=this.at(this.length-1);this.remove(b,a);return b},unshift:function(a,b){a=this._prepareModel(a,b);this.add(a,f.extend({at:0},
+b));return a},shift:function(a){var b=this.at(0);this.remove(b,a);return b},slice:function(a,b){return this.models.slice(a,b)},get:function(a){if(null!=a)return this._idAttr||(this._idAttr=this.model.prototype.idAttribute),this._byId[a.id||a.cid||a[this._idAttr]||a]},at:function(a){return this.models[a]},where:function(a){return f.isEmpty(a)?[]:this.filter(function(b){for(var c in a)if(a[c]!==b.get(c))return!1;return!0})},sort:function(a){if(!this.comparator)throw Error("Cannot sort a set without a comparator");
+a||(a={});f.isString(this.comparator)||1===this.comparator.length?this.models=this.sortBy(this.comparator,this):this.models.sort(f.bind(this.comparator,this));a.silent||this.trigger("sort",this,a);return this},pluck:function(a){return f.invoke(this.models,"get",a)},update:function(a,b){b=f.extend({add:!0,merge:!0,remove:!0},b);b.parse&&(a=this.parse(a,b));var c,d,e,g,h=[],j=[],l={};f.isArray(a)||(a=a?[a]:[]);if(b.add&&!b.remove)return this.add(a,b);d=0;for(e=a.length;d<e;d++)c=a[d],g=this.get(c),
+b.remove&&g&&(l[g.cid]=!0),(b.add&&!g||b.merge&&g)&&h.push(c);if(b.remove){d=0;for(e=this.models.length;d<e;d++)c=this.models[d],l[c.cid]||j.push(c)}j.length&&this.remove(j,b);h.length&&this.add(h,b);return this},reset:function(a,b){b||(b={});b.parse&&(a=this.parse(a,b));for(var c=0,d=this.models.length;c<d;c++)this._removeReference(this.models[c]);b.previousModels=this.models.slice();this._reset();a&&this.add(a,f.extend({silent:!0},b));b.silent||this.trigger("reset",this,b);return this},fetch:function(a){a=
+a?f.clone(a):{};void 0===a.parse&&(a.parse=!0);var b=a.success;a.success=function(a,d,e){a[e.update?"update":"reset"](d,e);b&&b(a,d,e)};return this.sync("read",this,a)},create:function(a,b){b=b?f.clone(b):{};if(!(a=this._prepareModel(a,b)))return!1;b.wait||this.add(a,b);var c=this,d=b.success;b.success=function(a,b,f){f.wait&&c.add(a,f);d&&d(a,b,f)};a.save(null,b);return a},parse:function(a){return a},clone:function(){return new this.constructor(this.models)},_reset:function(){this.length=0;this.models.length=
+0;this._byId={}},_prepareModel:function(a,b){if(a instanceof r)return a.collection||(a.collection=this),a;b||(b={});b.collection=this;var c=new this.model(a,b);return!c._validate(a,b)?!1:c},_removeReference:function(a){this===a.collection&&delete a.collection;a.off("all",this._onModelEvent,this)},_onModelEvent:function(a,b,c,d){("add"===a||"remove"===a)&&c!==this||("destroy"===a&&this.remove(b,d),b&&a==="change:"+b.idAttribute&&(delete this._byId[b.previous(b.idAttribute)],null!=b.id&&(this._byId[b.id]=
+b)),this.trigger.apply(this,arguments))},sortedIndex:function(a,b,c){b||(b=this.comparator);var d=f.isFunction(b)?b:function(a){return a.get(b)};return f.sortedIndex(this.models,a,d,c)}});f.each("forEach each map collect reduce foldl inject reduceRight foldr find detect filter select reject every all some any include contains invoke max min toArray size first head take initial rest tail drop last without indexOf shuffle lastIndexOf isEmpty chain".split(" "),function(a){s.prototype[a]=function(){var b=
+u.call(arguments);b.unshift(this.models);return f[a].apply(f,b)}});f.each(["groupBy","countBy","sortBy"],function(a){s.prototype[a]=function(b,c){var d=f.isFunction(b)?b:function(a){return a.get(b)};return f[a](this.models,d,c)}});var y=g.Router=function(a){a||(a={});a.routes&&(this.routes=a.routes);this._bindRoutes();this.initialize.apply(this,arguments)},E=/\((.*?)\)/g,F=/(\(\?)?:\w+/g,G=/\*\w+/g,H=/[\-{}\[\]+?.,\\\^$|#\s]/g;f.extend(y.prototype,h,{initialize:function(){},route:function(a,b,c){f.isRegExp(a)||
+(a=this._routeToRegExp(a));c||(c=this[b]);g.history.route(a,f.bind(function(d){d=this._extractParameters(a,d);c&&c.apply(this,d);this.trigger.apply(this,["route:"+b].concat(d));this.trigger("route",b,d);g.history.trigger("route",this,b,d)},this));return this},navigate:function(a,b){g.history.navigate(a,b);return this},_bindRoutes:function(){if(this.routes)for(var a,b=f.keys(this.routes);null!=(a=b.pop());)this.route(a,this.routes[a])},_routeToRegExp:function(a){a=a.replace(H,"\\$&").replace(E,"(?:$1)?").replace(F,
+function(a,c){return c?a:"([^/]+)"}).replace(G,"(.*?)");return RegExp("^"+a+"$")},_extractParameters:function(a,b){return a.exec(b).slice(1)}});var m=g.History=function(){this.handlers=[];f.bindAll(this,"checkUrl");"undefined"!==typeof window&&(this.location=window.location,this.history=window.history)},z=/^[#\/]|\s+$/g,I=/^\/+|\/+$/g,J=/msie [\w.]+/,K=/\/$/;m.started=!1;f.extend(m.prototype,h,{interval:50,getHash:function(a){return(a=(a||this).location.href.match(/#(.*)$/))?a[1]:""},getFragment:function(a,
+b){if(null==a)if(this._hasPushState||!this._wantsHashChange||b){a=this.location.pathname;var c=this.root.replace(K,"");a.indexOf(c)||(a=a.substr(c.length))}else a=this.getHash();return a.replace(z,"")},start:function(a){if(m.started)throw Error("Backbone.history has already been started");m.started=!0;this.options=f.extend({},{root:"/"},this.options,a);this.root=this.options.root;this._wantsHashChange=!1!==this.options.hashChange;this._wantsPushState=!!this.options.pushState;this._hasPushState=!(!this.options.pushState||
+!this.history||!this.history.pushState);a=this.getFragment();var b=document.documentMode,b=J.exec(navigator.userAgent.toLowerCase())&&(!b||7>=b);this.root=("/"+this.root+"/").replace(I,"/");b&&this._wantsHashChange&&(this.iframe=g.$('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo("body")[0].contentWindow,this.navigate(a));if(this._hasPushState)g.$(window).on("popstate",this.checkUrl);else if(this._wantsHashChange&&"onhashchange"in window&&!b)g.$(window).on("hashchange",this.checkUrl);
+else this._wantsHashChange&&(this._checkUrlInterval=setInterval(this.checkUrl,this.interval));this.fragment=a;a=this.location;b=a.pathname.replace(/[^\/]$/,"$&/")===this.root;if(this._wantsHashChange&&this._wantsPushState&&!this._hasPushState&&!b)return this.fragment=this.getFragment(null,!0),this.location.replace(this.root+this.location.search+"#"+this.fragment),!0;this._wantsPushState&&(this._hasPushState&&b&&a.hash)&&(this.fragment=this.getHash().replace(z,""),this.history.replaceState({},document.title,
+this.root+this.fragment+a.search));if(!this.options.silent)return this.loadUrl()},stop:function(){g.$(window).off("popstate",this.checkUrl).off("hashchange",this.checkUrl);clearInterval(this._checkUrlInterval);m.started=!1},route:function(a,b){this.handlers.unshift({route:a,callback:b})},checkUrl:function(){var a=this.getFragment();a===this.fragment&&this.iframe&&(a=this.getFragment(this.getHash(this.iframe)));if(a===this.fragment)return!1;this.iframe&&this.navigate(a);this.loadUrl()||this.loadUrl(this.getHash())},
+loadUrl:function(a){var b=this.fragment=this.getFragment(a);return f.any(this.handlers,function(a){if(a.route.test(b))return a.callback(b),!0})},navigate:function(a,b){if(!m.started)return!1;if(!b||!0===b)b={trigger:b};a=this.getFragment(a||"");if(this.fragment!==a){this.fragment=a;var c=this.root+a;if(this._hasPushState)this.history[b.replace?"replaceState":"pushState"]({},document.title,c);else if(this._wantsHashChange)this._updateHash(this.location,a,b.replace),this.iframe&&a!==this.getFragment(this.getHash(this.iframe))&&
+(b.replace||this.iframe.document.open().close(),this._updateHash(this.iframe.location,a,b.replace));else return this.location.assign(c);b.trigger&&this.loadUrl(a)}},_updateHash:function(a,b,c){c?(c=a.href.replace(/(javascript:|#).*$/,""),a.replace(c+"#"+b)):a.hash="#"+b}});g.history=new m;var A=g.View=function(a){this.cid=f.uniqueId("view");this._configure(a||{});this._ensureElement();this.initialize.apply(this,arguments);this.delegateEvents()},L=/^(\S+)\s*(.*)$/,M="model collection el id attributes className tagName events".split(" ");
+f.extend(A.prototype,h,{tagName:"div",$:function(a){return this.$el.find(a)},initialize:function(){},render:function(){return this},remove:function(){this.$el.remove();this.stopListening();return this},setElement:function(a,b){this.$el&&this.undelegateEvents();this.$el=a instanceof g.$?a:g.$(a);this.el=this.$el[0];!1!==b&&this.delegateEvents();return this},delegateEvents:function(a){if(a||(a=f.result(this,"events"))){this.undelegateEvents();for(var b in a){var c=a[b];f.isFunction(c)||(c=this[a[b]]);
+if(!c)throw Error('Method "'+a[b]+'" does not exist');var d=b.match(L),e=d[1],d=d[2],c=f.bind(c,this),e=e+(".delegateEvents"+this.cid);if(""===d)this.$el.on(e,c);else this.$el.on(e,d,c)}}},undelegateEvents:function(){this.$el.off(".delegateEvents"+this.cid)},_configure:function(a){this.options&&(a=f.extend({},f.result(this,"options"),a));f.extend(this,f.pick(a,M));this.options=a},_ensureElement:function(){if(this.el)this.setElement(f.result(this,"el"),!1);else{var a=f.extend({},f.result(this,"attributes"));
+this.id&&(a.id=f.result(this,"id"));this.className&&(a["class"]=f.result(this,"className"));a=g.$("<"+f.result(this,"tagName")+">").attr(a);this.setElement(a,!1)}}});var N={create:"POST",update:"PUT",patch:"PATCH","delete":"DELETE",read:"GET"};g.sync=function(a,b,c){var d=N[a];f.defaults(c||(c={}),{emulateHTTP:g.emulateHTTP,emulateJSON:g.emulateJSON});var e={type:d,dataType:"json"};c.url||(e.url=f.result(b,"url")||x());if(null==c.data&&b&&("create"===a||"update"===a||"patch"===a))e.contentType="application/json",
+e.data=JSON.stringify(c.attrs||b.toJSON(c));c.emulateJSON&&(e.contentType="application/x-www-form-urlencoded",e.data=e.data?{model:e.data}:{});if(c.emulateHTTP&&("PUT"===d||"DELETE"===d||"PATCH"===d)){e.type="POST";c.emulateJSON&&(e.data._method=d);var h=c.beforeSend;c.beforeSend=function(a){a.setRequestHeader("X-HTTP-Method-Override",d);if(h)return h.apply(this,arguments)}}"GET"!==e.type&&!c.emulateJSON&&(e.processData=!1);var m=c.success;c.success=function(a){m&&m(b,a,c);b.trigger("sync",b,a,c)};
+var j=c.error;c.error=function(a){j&&j(b,a,c);b.trigger("error",b,a,c)};a=c.xhr=g.ajax(f.extend(e,c));b.trigger("request",b,a,c);return a};g.ajax=function(){return g.$.ajax.apply(g.$,arguments)};r.extend=s.extend=y.extend=A.extend=m.extend=function(a,b){var c=this,d;d=a&&f.has(a,"constructor")?a.constructor:function(){return c.apply(this,arguments)};f.extend(d,c,b);var e=function(){this.constructor=d};e.prototype=c.prototype;d.prototype=new e;a&&f.extend(d.prototype,a);d.__super__=c.prototype;return d};
+var x=function(){throw Error('A "url" property or function must be specified');}}).call(this);
diff --git a/timer/static/js/underscore-min.js b/timer/static/js/underscore-min.js
new file mode 100644
index 0000000..7ed6e52
--- /dev/null
+++ b/timer/static/js/underscore-min.js
@@ -0,0 +1 @@
+(function(){var n=this,t=n._,r={},e=Array.prototype,u=Object.prototype,i=Function.prototype,a=e.push,o=e.slice,c=e.concat,l=u.toString,f=u.hasOwnProperty,s=e.forEach,p=e.map,v=e.reduce,h=e.reduceRight,g=e.filter,d=e.every,m=e.some,y=e.indexOf,b=e.lastIndexOf,x=Array.isArray,_=Object.keys,j=i.bind,w=function(n){return n instanceof w?n:this instanceof w?(this._wrapped=n,void 0):new w(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=w),exports._=w):n._=w,w.VERSION="1.4.3";var A=w.each=w.forEach=function(n,t,e){if(null!=n)if(s&&n.forEach===s)n.forEach(t,e);else if(n.length===+n.length){for(var u=0,i=n.length;i>u;u++)if(t.call(e,n[u],u,n)===r)return}else for(var a in n)if(w.has(n,a)&&t.call(e,n[a],a,n)===r)return};w.map=w.collect=function(n,t,r){var e=[];return null==n?e:p&&n.map===p?n.map(t,r):(A(n,function(n,u,i){e[e.length]=t.call(r,n,u,i)}),e)};var O="Reduce of empty array with no initial value";w.reduce=w.foldl=w.inject=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),v&&n.reduce===v)return e&&(t=w.bind(t,e)),u?n.reduce(t,r):n.reduce(t);if(A(n,function(n,i,a){u?r=t.call(e,r,n,i,a):(r=n,u=!0)}),!u)throw new TypeError(O);return r},w.reduceRight=w.foldr=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),h&&n.reduceRight===h)return e&&(t=w.bind(t,e)),u?n.reduceRight(t,r):n.reduceRight(t);var i=n.length;if(i!==+i){var a=w.keys(n);i=a.length}if(A(n,function(o,c,l){c=a?a[--i]:--i,u?r=t.call(e,r,n[c],c,l):(r=n[c],u=!0)}),!u)throw new TypeError(O);return r},w.find=w.detect=function(n,t,r){var e;return E(n,function(n,u,i){return t.call(r,n,u,i)?(e=n,!0):void 0}),e},w.filter=w.select=function(n,t,r){var e=[];return null==n?e:g&&n.filter===g?n.filter(t,r):(A(n,function(n,u,i){t.call(r,n,u,i)&&(e[e.length]=n)}),e)},w.reject=function(n,t,r){return w.filter(n,function(n,e,u){return!t.call(r,n,e,u)},r)},w.every=w.all=function(n,t,e){t||(t=w.identity);var u=!0;return null==n?u:d&&n.every===d?n.every(t,e):(A(n,function(n,i,a){return(u=u&&t.call(e,n,i,a))?void 0:r}),!!u)};var E=w.some=w.any=function(n,t,e){t||(t=w.identity);var u=!1;return null==n?u:m&&n.some===m?n.some(t,e):(A(n,function(n,i,a){return u||(u=t.call(e,n,i,a))?r:void 0}),!!u)};w.contains=w.include=function(n,t){return null==n?!1:y&&n.indexOf===y?-1!=n.indexOf(t):E(n,function(n){return n===t})},w.invoke=function(n,t){var r=o.call(arguments,2);return w.map(n,function(n){return(w.isFunction(t)?t:n[t]).apply(n,r)})},w.pluck=function(n,t){return w.map(n,function(n){return n[t]})},w.where=function(n,t){return w.isEmpty(t)?[]:w.filter(n,function(n){for(var r in t)if(t[r]!==n[r])return!1;return!0})},w.max=function(n,t,r){if(!t&&w.isArray(n)&&n[0]===+n[0]&&65535>n.length)return Math.max.apply(Math,n);if(!t&&w.isEmpty(n))return-1/0;var e={computed:-1/0,value:-1/0};return A(n,function(n,u,i){var a=t?t.call(r,n,u,i):n;a>=e.computed&&(e={value:n,computed:a})}),e.value},w.min=function(n,t,r){if(!t&&w.isArray(n)&&n[0]===+n[0]&&65535>n.length)return Math.min.apply(Math,n);if(!t&&w.isEmpty(n))return 1/0;var e={computed:1/0,value:1/0};return A(n,function(n,u,i){var a=t?t.call(r,n,u,i):n;e.computed>a&&(e={value:n,computed:a})}),e.value},w.shuffle=function(n){var t,r=0,e=[];return A(n,function(n){t=w.random(r++),e[r-1]=e[t],e[t]=n}),e};var F=function(n){return w.isFunction(n)?n:function(t){return t[n]}};w.sortBy=function(n,t,r){var e=F(t);return w.pluck(w.map(n,function(n,t,u){return{value:n,index:t,criteria:e.call(r,n,t,u)}}).sort(function(n,t){var r=n.criteria,e=t.criteria;if(r!==e){if(r>e||void 0===r)return 1;if(e>r||void 0===e)return-1}return n.index<t.index?-1:1}),"value")};var k=function(n,t,r,e){var u={},i=F(t||w.identity);return A(n,function(t,a){var o=i.call(r,t,a,n);e(u,o,t)}),u};w.groupBy=function(n,t,r){return k(n,t,r,function(n,t,r){(w.has(n,t)?n[t]:n[t]=[]).push(r)})},w.countBy=function(n,t,r){return k(n,t,r,function(n,t){w.has(n,t)||(n[t]=0),n[t]++})},w.sortedIndex=function(n,t,r,e){r=null==r?w.identity:F(r);for(var u=r.call(e,t),i=0,a=n.length;a>i;){var o=i+a>>>1;u>r.call(e,n[o])?i=o+1:a=o}return i},w.toArray=function(n){return n?w.isArray(n)?o.call(n):n.length===+n.length?w.map(n,w.identity):w.values(n):[]},w.size=function(n){return null==n?0:n.length===+n.length?n.length:w.keys(n).length},w.first=w.head=w.take=function(n,t,r){return null==n?void 0:null==t||r?n[0]:o.call(n,0,t)},w.initial=function(n,t,r){return o.call(n,0,n.length-(null==t||r?1:t))},w.last=function(n,t,r){return null==n?void 0:null==t||r?n[n.length-1]:o.call(n,Math.max(n.length-t,0))},w.rest=w.tail=w.drop=function(n,t,r){return o.call(n,null==t||r?1:t)},w.compact=function(n){return w.filter(n,w.identity)};var R=function(n,t,r){return A(n,function(n){w.isArray(n)?t?a.apply(r,n):R(n,t,r):r.push(n)}),r};w.flatten=function(n,t){return R(n,t,[])},w.without=function(n){return w.difference(n,o.call(arguments,1))},w.uniq=w.unique=function(n,t,r,e){w.isFunction(t)&&(e=r,r=t,t=!1);var u=r?w.map(n,r,e):n,i=[],a=[];return A(u,function(r,e){(t?e&&a[a.length-1]===r:w.contains(a,r))||(a.push(r),i.push(n[e]))}),i},w.union=function(){return w.uniq(c.apply(e,arguments))},w.intersection=function(n){var t=o.call(arguments,1);return w.filter(w.uniq(n),function(n){return w.every(t,function(t){return w.indexOf(t,n)>=0})})},w.difference=function(n){var t=c.apply(e,o.call(arguments,1));return w.filter(n,function(n){return!w.contains(t,n)})},w.zip=function(){for(var n=o.call(arguments),t=w.max(w.pluck(n,"length")),r=Array(t),e=0;t>e;e++)r[e]=w.pluck(n,""+e);return r},w.object=function(n,t){if(null==n)return{};for(var r={},e=0,u=n.length;u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},w.indexOf=function(n,t,r){if(null==n)return-1;var e=0,u=n.length;if(r){if("number"!=typeof r)return e=w.sortedIndex(n,t),n[e]===t?e:-1;e=0>r?Math.max(0,u+r):r}if(y&&n.indexOf===y)return n.indexOf(t,r);for(;u>e;e++)if(n[e]===t)return e;return-1},w.lastIndexOf=function(n,t,r){if(null==n)return-1;var e=null!=r;if(b&&n.lastIndexOf===b)return e?n.lastIndexOf(t,r):n.lastIndexOf(t);for(var u=e?r:n.length;u--;)if(n[u]===t)return u;return-1},w.range=function(n,t,r){1>=arguments.length&&(t=n||0,n=0),r=arguments[2]||1;for(var e=Math.max(Math.ceil((t-n)/r),0),u=0,i=Array(e);e>u;)i[u++]=n,n+=r;return i};var I=function(){};w.bind=function(n,t){var r,e;if(n.bind===j&&j)return j.apply(n,o.call(arguments,1));if(!w.isFunction(n))throw new TypeError;return r=o.call(arguments,2),e=function(){if(!(this instanceof e))return n.apply(t,r.concat(o.call(arguments)));I.prototype=n.prototype;var u=new I;I.prototype=null;var i=n.apply(u,r.concat(o.call(arguments)));return Object(i)===i?i:u}},w.bindAll=function(n){var t=o.call(arguments,1);return 0==t.length&&(t=w.functions(n)),A(t,function(t){n[t]=w.bind(n[t],n)}),n},w.memoize=function(n,t){var r={};return t||(t=w.identity),function(){var e=t.apply(this,arguments);return w.has(r,e)?r[e]:r[e]=n.apply(this,arguments)}},w.delay=function(n,t){var r=o.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},w.defer=function(n){return w.delay.apply(w,[n,1].concat(o.call(arguments,1)))},w.throttle=function(n,t){var r,e,u,i,a=0,o=function(){a=new Date,u=null,i=n.apply(r,e)};return function(){var c=new Date,l=t-(c-a);return r=this,e=arguments,0>=l?(clearTimeout(u),u=null,a=c,i=n.apply(r,e)):u||(u=setTimeout(o,l)),i}},w.debounce=function(n,t,r){var e,u;return function(){var i=this,a=arguments,o=function(){e=null,r||(u=n.apply(i,a))},c=r&&!e;return clearTimeout(e),e=setTimeout(o,t),c&&(u=n.apply(i,a)),u}},w.once=function(n){var t,r=!1;return function(){return r?t:(r=!0,t=n.apply(this,arguments),n=null,t)}},w.wrap=function(n,t){return function(){var r=[n];return a.apply(r,arguments),t.apply(this,r)}},w.compose=function(){var n=arguments;return function(){for(var t=arguments,r=n.length-1;r>=0;r--)t=[n[r].apply(this,t)];return t[0]}},w.after=function(n,t){return 0>=n?t():function(){return 1>--n?t.apply(this,arguments):void 0}},w.keys=_||function(n){if(n!==Object(n))throw new TypeError("Invalid object");var t=[];for(var r in n)w.has(n,r)&&(t[t.length]=r);return t},w.values=function(n){var t=[];for(var r in n)w.has(n,r)&&t.push(n[r]);return t},w.pairs=function(n){var t=[];for(var r in n)w.has(n,r)&&t.push([r,n[r]]);return t},w.invert=function(n){var t={};for(var r in n)w.has(n,r)&&(t[n[r]]=r);return t},w.functions=w.methods=function(n){var t=[];for(var r in n)w.isFunction(n[r])&&t.push(r);return t.sort()},w.extend=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)n[r]=t[r]}),n},w.pick=function(n){var t={},r=c.apply(e,o.call(arguments,1));return A(r,function(r){r in n&&(t[r]=n[r])}),t},w.omit=function(n){var t={},r=c.apply(e,o.call(arguments,1));for(var u in n)w.contains(r,u)||(t[u]=n[u]);return t},w.defaults=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)null==n[r]&&(n[r]=t[r])}),n},w.clone=function(n){return w.isObject(n)?w.isArray(n)?n.slice():w.extend({},n):n},w.tap=function(n,t){return t(n),n};var S=function(n,t,r,e){if(n===t)return 0!==n||1/n==1/t;if(null==n||null==t)return n===t;n instanceof w&&(n=n._wrapped),t instanceof w&&(t=t._wrapped);var u=l.call(n);if(u!=l.call(t))return!1;switch(u){case"[object String]":return n==t+"";case"[object Number]":return n!=+n?t!=+t:0==n?1/n==1/t:n==+t;case"[object Date]":case"[object Boolean]":return+n==+t;case"[object RegExp]":return n.source==t.source&&n.global==t.global&&n.multiline==t.multiline&&n.ignoreCase==t.ignoreCase}if("object"!=typeof n||"object"!=typeof t)return!1;for(var i=r.length;i--;)if(r[i]==n)return e[i]==t;r.push(n),e.push(t);var a=0,o=!0;if("[object Array]"==u){if(a=n.length,o=a==t.length)for(;a--&&(o=S(n[a],t[a],r,e)););}else{var c=n.constructor,f=t.constructor;if(c!==f&&!(w.isFunction(c)&&c instanceof c&&w.isFunction(f)&&f instanceof f))return!1;for(var s in n)if(w.has(n,s)&&(a++,!(o=w.has(t,s)&&S(n[s],t[s],r,e))))break;if(o){for(s in t)if(w.has(t,s)&&!a--)break;o=!a}}return r.pop(),e.pop(),o};w.isEqual=function(n,t){return S(n,t,[],[])},w.isEmpty=function(n){if(null==n)return!0;if(w.isArray(n)||w.isString(n))return 0===n.length;for(var t in n)if(w.has(n,t))return!1;return!0},w.isElement=function(n){return!(!n||1!==n.nodeType)},w.isArray=x||function(n){return"[object Array]"==l.call(n)},w.isObject=function(n){return n===Object(n)},A(["Arguments","Function","String","Number","Date","RegExp"],function(n){w["is"+n]=function(t){return l.call(t)=="[object "+n+"]"}}),w.isArguments(arguments)||(w.isArguments=function(n){return!(!n||!w.has(n,"callee"))}),w.isFunction=function(n){return"function"==typeof n},w.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},w.isNaN=function(n){return w.isNumber(n)&&n!=+n},w.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"==l.call(n)},w.isNull=function(n){return null===n},w.isUndefined=function(n){return void 0===n},w.has=function(n,t){return f.call(n,t)},w.noConflict=function(){return n._=t,this},w.identity=function(n){return n},w.times=function(n,t,r){for(var e=Array(n),u=0;n>u;u++)e[u]=t.call(r,u);return e},w.random=function(n,t){return null==t&&(t=n,n=0),n+(0|Math.random()*(t-n+1))};var T={escape:{"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#x27;","/":"&#x2F;"}};T.unescape=w.invert(T.escape);var M={escape:RegExp("["+w.keys(T.escape).join("")+"]","g"),unescape:RegExp("("+w.keys(T.unescape).join("|")+")","g")};w.each(["escape","unescape"],function(n){w[n]=function(t){return null==t?"":(""+t).replace(M[n],function(t){return T[n][t]})}}),w.result=function(n,t){if(null==n)return null;var r=n[t];return w.isFunction(r)?r.call(n):r},w.mixin=function(n){A(w.functions(n),function(t){var r=w[t]=n[t];w.prototype[t]=function(){var n=[this._wrapped];return a.apply(n,arguments),z.call(this,r.apply(w,n))}})};var N=0;w.uniqueId=function(n){var t=""+ ++N;return n?n+t:t},w.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var q=/(.)^/,B={"'":"'","\\":"\\","\r":"r","\n":"n"," ":"t","\u2028":"u2028","\u2029":"u2029"},D=/\\|'|\r|\n|\t|\u2028|\u2029/g;w.template=function(n,t,r){r=w.defaults({},r,w.templateSettings);var e=RegExp([(r.escape||q).source,(r.interpolate||q).source,(r.evaluate||q).source].join("|")+"|$","g"),u=0,i="__p+='";n.replace(e,function(t,r,e,a,o){return i+=n.slice(u,o).replace(D,function(n){return"\\"+B[n]}),r&&(i+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'"),e&&(i+="'+\n((__t=("+e+"))==null?'':__t)+\n'"),a&&(i+="';\n"+a+"\n__p+='"),u=o+t.length,t}),i+="';\n",r.variable||(i="with(obj||{}){\n"+i+"}\n"),i="var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');};\n"+i+"return __p;\n";try{var a=Function(r.variable||"obj","_",i)}catch(o){throw o.source=i,o}if(t)return a(t,w);var c=function(n){return a.call(this,n,w)};return c.source="function("+(r.variable||"obj")+"){\n"+i+"}",c},w.chain=function(n){return w(n).chain()};var z=function(n){return this._chain?w(n).chain():n};w.mixin(w),A(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=e[n];w.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!=n&&"splice"!=n||0!==r.length||delete r[0],z.call(this,r)}}),A(["concat","join","slice"],function(n){var t=e[n];w.prototype[n]=function(){return z.call(this,t.apply(this._wrapped,arguments))}}),w.extend(w.prototype,{chain:function(){return this._chain=!0,this},value:function(){return this._wrapped}})}).call(this); \ No newline at end of file
diff --git a/timer/templates/alarm.html b/timer/templates/alarm.html
new file mode 100644
index 0000000..a9a6b54
--- /dev/null
+++ b/timer/templates/alarm.html
@@ -0,0 +1,68 @@
+{% extends "default.html" %}
+
+{% block js %}
+ <script type="text/template" id="startViewTemplate">
+ <input type="text" value="<%= minutes %>" placeholder="M" id="minutes"/>:<input type="text" id="seconds" value="<%= seconds %>"/>
+ <a href="#runningView" data-role="button">Start</a>
+ </script>
+ <script type="text/template" id="runningViewTemplate">
+ <h1><%= minutes_remaining %>:<%= seconds_remaining %></h1>
+ <a href="#" data-role="button">Stop</a>
+ </script>
+ <script type="text/javascript">
+ var Alarm = Backbone.Model.extend({
+ start: function() {
+ var elapsed = 0;
+ var interval = parseInt(this.get('minutes'))*60 + parseInt(this.get('seconds'));
+ window.setInterval(function() {
+ elapsed += 1;
+ if(elapsed < interval) {
+ console.log(interval - elapsed, ' seconds to go');
+ }
+ }, 1000);
+ },
+ stop: function() {
+
+ },
+ pause: function() {
+
+ }
+ });
+
+ var RunningView = Backbone.View.extend({
+ initialize: function() {
+ this.template = _.template($('#runningViewTemplate').html());
+ this.modal.on('change', this.render);
+ },
+ render: function() {
+ console.log('render!!!');
+ return this;
+ }
+ });
+
+ var AppView = Backbone.View.extend({
+ initialize: function() {
+ var tpl = _.template($('#startViewTemplate').html());
+ alarm = new Alarm({minutes: 0, seconds: 15});
+ $('#startView').append(tpl(alarm.toJSON()));
+ //alarm.start();
+ //$('#startView a').button('refresh');
+ //$('#startView input').textinput('enable');
+ }
+ });
+
+ $(function() {
+ new AppView;
+ });
+
+ </script>
+{% endblock js %}
+
+{% block content %}
+ <div data-role="page">
+ <div data-role="content" id="startView"></div>
+ </div>
+ <div data-role="page">
+ <div data-role="content" id="runningView"></div>
+ </div>
+{% endblock content %}
diff --git a/timer/templates/default.html b/timer/templates/default.html
new file mode 100644
index 0000000..ec91d78
--- /dev/null
+++ b/timer/templates/default.html
@@ -0,0 +1,62 @@
+{% load staticfiles %}
+<!DOCTYPE html>
+<html>
+<head>
+ <title>opus v0.01</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <link rel="stylesheet" href="http://code.jquery.com/mobile/1.3.0-beta.1/jquery.mobile-1.3.0-beta.1.min.css" />
+ <script src="http://code.jquery.com/jquery-1.8.3.min.js"></script>
+ <script src="http://code.jquery.com/mobile/1.3.0-beta.1/jquery.mobile-1.3.0-beta.1.min.js"></script>
+ <script type="text/javascript">
+ function getDate(field) {
+ // 2013-01-30 08:36:48
+ var splitRex = /^(\d{4})\-(\d{2})\-(\d{2})\s(\d{2}):(\d{2}):(\d{2})$/;
+ var s = $(field).val().match(splitRex);
+ return new Date(s[1], parseInt(s[2])-1, s[3], s[4], s[5], s[6], 0);
+ }
+
+ function toDateTime(date) {
+ function pad(n){return n<10 ? '0'+n : n}
+ return date.getFullYear()+'-'
+ +pad(date.getMonth()+1)+'-'
+ +pad(date.getDate())+' '
+ +pad(date.getHours())+':'
+ +pad(date.getMinutes())+':'
+ +pad(date.getSeconds());
+ }
+
+ $(function(){
+ $('#duration').blur(function(){
+ if($('#id_started_at').val() == '') return;
+
+ started_at = getDate('#id_started_at');
+ var d = $(this).val().split(':');
+ // H:M:S
+ var seconds = 0;
+ if (d[0]) seconds += parseInt(d[0]*3600);
+ if (d[1]) seconds += parseInt(d[1]*60);
+ if (d[2]) seconds += parseInt(d[2]);
+ finished_at = new Date(started_at.getTime() + (seconds*1000));
+ $('#id_finished_at').val(toDateTime(finished_at));
+ $('#id_duration').val(seconds);
+
+ });
+
+ $('#id_finished_at').blur(function(){
+ if($('#id_duration').val() == '') return;
+ finished_at = getDate('#id_finished_at');
+ started_at = getDate('#id_started_at');
+ seconds = (finished_at - started_at)/1000;
+ $('#id_duration').val(seconds);
+ hours = seconds/3600;
+ $('#duration').val(hours);
+ });
+ });
+ </script>
+</head>
+ <body id="app">
+ {% block content %}
+
+ {% endblock content %}
+ </body>
+</html>
diff --git a/timer/templates/event_detail.html b/timer/templates/event_detail.html
new file mode 100644
index 0000000..8f32759
--- /dev/null
+++ b/timer/templates/event_detail.html
@@ -0,0 +1,13 @@
+{% extends "timer.html" %}
+
+{% block content %}
+ <div data-role="page">
+ <div data-role="header" data-position="fixed">
+ <a href="#" data-icon="back" data-rel="back">Back</a>
+ <h1></h1>
+ </div>
+ <div data-role="content">
+ <a href="{{ event.get_absolute_url }}delete/" data-role="button">Delete</a>
+ </div>
+ </div>
+{% endblock content %}
diff --git a/timer/templates/event_grid.html b/timer/templates/event_grid.html
new file mode 100644
index 0000000..efc8a39
--- /dev/null
+++ b/timer/templates/event_grid.html
@@ -0,0 +1,30 @@
+{% extends "timer.html" %}
+
+{% block main_content %}
+<table style="width:100%;border:1px; solid #ccc">
+ <thead>
+ <tr>
+ <th>Date</th>
+ <th>Started</th>
+ <th>Finished</th>
+ <th>Total</th>
+ <th>Week Total</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for e in events %}
+ <tr>
+ <td>{{ started_at|date:"SHORT_DATE_FORMAT" }}</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ <tfoot>
+ <tr>
+ <td>Normal Hours:</td>
+ <td>{{ normal_hours }}</td>
+ <td>Over Time:</td>
+ <td>{{ over_time }}</td>
+ </tr>
+ </tfoot>
+</table>
+{% endblock main_content %}
diff --git a/timer/templates/event_list.html b/timer/templates/event_list.html
new file mode 100644
index 0000000..033f004
--- /dev/null
+++ b/timer/templates/event_list.html
@@ -0,0 +1,5 @@
+{% for i in events %}
+ <li>
+ <a href="{{ i.get_absolute_url }}">{{ i.hours }}h @ {{ i.started_at|date:"SHORT_DATE_FORMAT" }} {{ i.started_at|date:"H:i" }} {{ i.notes }}</a>
+ </li>
+{% endfor %}
diff --git a/timer/templates/label_list.html b/timer/templates/label_list.html
new file mode 100644
index 0000000..2f044be
--- /dev/null
+++ b/timer/templates/label_list.html
@@ -0,0 +1,3 @@
+{% for l in labels %}
+ <li><a href="/timer/label/1/events/?view=list">{{ l.title }}<span class="ui-li-count">{{ l.event_set.count }}</span></a></a></li>
+{% endfor %}
diff --git a/timer/templates/labels.html b/timer/templates/labels.html
new file mode 100644
index 0000000..325f5e7
--- /dev/null
+++ b/timer/templates/labels.html
@@ -0,0 +1,31 @@
+{% extends "timer.html" %}
+
+{% block content %}
+<div data-role="page">
+ <div data-role="header">
+ <h1>Labels</h1>
+ <a href="#labelPopup" data-icon="plus" data-rel="popup" data-position-to="window">Add</a>
+ </div>
+ <div data-role="content">
+ <ul data-role="listview" id="labelList">
+ {% include "label_list.html" %}
+ </ul>
+ </div>
+ <div data-role="popup" id="labelPopup" class="ui-content">
+ <form method="post" action="/timer/labels/new/">
+ {% csrf_token %}
+ <input type="text" name="title"/>
+ <select name="color">
+ <option>Red</option>
+ <option>Orange</option>
+ <option>Yellow</option>
+ <option>Green</option>
+ <option>Blue</option>
+ <option>Purple</option>
+ <option>Brown</option>
+ </select>
+ <button type="submit">Save</button>
+ </form>
+ </div>
+</div>
+{% endblock content %}
diff --git a/timer/templates/timer.html b/timer/templates/timer.html
new file mode 100644
index 0000000..bb1d0c0
--- /dev/null
+++ b/timer/templates/timer.html
@@ -0,0 +1,59 @@
+{% extends "default.html" %}
+
+{% block content %}
+<div data-role="page">
+ <div data-role="header" data-position="fixed">
+ <a href="/timer/">Labels</a>
+ <h1>{{ label.title }}</h1>
+ <a href="#eventPopup" data-icon="plus" data-rel="popup" data-position-to="window" data-transition="fade">New Event</a>
+ </div>
+ <div data-role="header" data-theme="b">
+ <a href="?start={{ previous }}" data-icon="arrow-l" data-iconpos="left">Previous</a>
+ <h1>{{ title|date:"SHORT_DATE_FORMAT" }}</h1>
+ <a href="?start={{ next }} " data-icon="arrow-r" data-iconpos="right">Next</a>
+ </div>
+ <div data-role="navbar">
+ <ul>
+ <li><a href="?view=list">List</a></li>
+ <li><a href="?view=report">Report</a></li>
+ </ul>
+ </div>
+ <div data-role="content">
+ {% block main_content %}
+ <ul data-role="listview" id="eventList" data-filter="true">
+ {% include "event_list.html" %}
+ </ul>
+ {% endblock main_content %}
+ </div>
+
+ <div data-role="footer" data-position="fixed" style="text-align:center;">
+ <a href="?start={% now "Ymd" %}&amp;group=day" style="float:left">Today</a>
+ <div data-role="controlgroup" data-type="horizontal">
+ <a href="?start={{ title|date:"Ymd" }}&amp;group=day" data-role="button">Day</a>
+ <a href="?start={{ title|date:"Ymd" }}&amp;group=week" data-role="button">Week</a>
+ <a href="?start={{ title|date:"Ymd" }}&amp;group=month" data-role="button">Month</a>
+ </div>
+ </div>
+
+ <div data-role="popup" id="eventPopup" class="ui-content" data-theme="b">
+ <a href="#" data-rel="back" data-role="button" data-theme="a" data-icon="delete" data-iconpos="notext" class="ui-btn-right">Close</a>
+ <form method="post" action="" class="ui-hide-label">
+ {% csrf_token %}
+ <div class="ui-grid-b">
+ <div class="ui-block-a">{{ form.started_at }}</div>
+ <div class="ui-block-c"><input type="text" id="duration"/></div>
+ <div class="ui-block-b">{{ form.finished_at }}</div>
+ </div><!-- /grid-a -->
+ {{ form.duration }}
+ {{ form.labels }}
+ {{ form.notes }}
+ <button type="submit" data-theme="b">Save</button>
+ </form>
+ </div>
+</div>
+
+<div data-role="page" id="event">
+
+</div>
+
+{% endblock content %}
diff --git a/timer/tests.py b/timer/tests.py
new file mode 100644
index 0000000..501deb7
--- /dev/null
+++ b/timer/tests.py
@@ -0,0 +1,16 @@
+"""
+This file demonstrates writing tests using the unittest module. These will pass
+when you run "manage.py test".
+
+Replace this with more appropriate tests for your application.
+"""
+
+from django.test import TestCase
+
+
+class SimpleTest(TestCase):
+ def test_basic_addition(self):
+ """
+ Tests that 1 + 1 always equals 2.
+ """
+ self.assertEqual(1 + 1, 2)
diff --git a/timer/views.py b/timer/views.py
new file mode 100644
index 0000000..8c1414e
--- /dev/null
+++ b/timer/views.py
@@ -0,0 +1,102 @@
+import json
+from datetime import datetime, timedelta
+
+from django import forms
+from django.http import HttpResponse
+from django.shortcuts import redirect, render
+
+from timer.models import Event, Label
+
+class LabelForm(forms.ModelForm):
+ class Meta:
+ model = Label
+
+class EventForm(forms.ModelForm):
+ class Meta:
+ model = Event
+ widgets = {
+ 'duration': forms.HiddenInput()
+ }
+
+def go(request):
+ return render(request, 'timer.html')
+
+def events(request, label, event_id=None):
+
+ fmt = '%Y%m%d'
+ label = Label.objects.get(pk=label)
+
+ if request.method == 'POST':
+ event = Event(user_id=1)
+ form = EventForm(request.POST, instance=event)
+ if form.is_valid():
+ form.save()
+ else:
+ print form.errors
+
+ start = datetime.now()
+ events = Event.objects.filter(labels=label)
+
+ if request.GET.get('start'):
+ start = datetime.strptime(request.GET.get('start'), fmt)
+ events = events.filter(started_at__lte=start)
+
+ delta = timedelta(days=1)
+
+ if request.GET.get('group') == 'week':
+ delta = timedelta(days=7)
+
+ if request.GET.get('group') == 'month':
+ delta = timedelta(days=30) # BULLSHIT!
+
+ try:
+ now = events[0].started_at
+ except IndexError, e:
+ now = start
+
+ next = now + delta
+ previous = now - delta
+
+ form = EventForm(initial={'labels': [label]})
+
+ if request.GET.get('view') == 'report':
+ return render(request, 'event_grid.html', {'events': events})
+
+ return render(request, 'timer.html', {
+ 'events': events,
+ 'label': label,
+ 'form': form,
+ 'title': start,
+ 'previous': previous.strftime(fmt),
+ 'next': next.strftime(fmt),
+ })
+
+def delete_event(request, event_id):
+ Event.objects.filter(pk=event_id).delete()
+ return HttpResponse('OK')
+
+def labels(request):
+ labels = Label.objects.filter(user_id=1)
+ return render(request, 'labels.html', {'labels': labels})
+
+def alarm(request):
+ return render(request, 'alarm.html')
+
+def edit_label(request, label_id=None):
+
+ label = Label(user_id=1)
+
+ if label_id:
+ label = Label.objects.get(pk=label_id)
+
+ if request.method == 'POST':
+ form = LabelForm(request.POST, instance=label)
+ if form.is_valid():
+ label = form.save()
+ else:
+ print form.errors
+
+ return redirect(label)
+
+def view_event(request, event_id):
+ return render(request, 'event_detail.html')