Basic styling, lots of feature work

This commit is contained in:
2013-05-12 17:12:48 +01:00
parent 33cf34ba92
commit a644b280eb
24 changed files with 10681 additions and 24 deletions

3
.gitignore vendored
View File

@@ -1,2 +1,3 @@
.idea
*.db3
*.db3
*.pyc

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

6158
app/limetime/static/css/bootstrap.css vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

2276
app/limetime/static/js/bootstrap.js vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,48 @@
{% load staticfiles %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Timer</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="">
<meta name="author" content="">
<link href="{% static "css/bootstrap.css" %}" rel="stylesheet">
<style>
body {
padding-top: 60px; /* 60px to make the container go all the way to the bottom of the topbar */
}
</style>
<link href="{% static "css/bootstrap-responsive.css" %}" rel="stylesheet">
</head>
<body>
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="navbar-inner">
<div class="container">
<button type="button" class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="brand" href="#">Timer</a>
<div class="nav-collapse collapse">
<ul class="nav">
<li class="active"><a href="{% url "timer-list" %}">Timers</a></li>
<li class=""><a href="{% url "timer-create" %}">Add</a></li>
</ul>
</div><!--/.nav-collapse -->
</div>
</div>
</div>
<div class="container">
{% block content %}
{% endblock %}
</div>
<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
{% block scripts %}
{% endblock %}
</body>
</html>

10
app/timer/forms.py Normal file
View File

@@ -0,0 +1,10 @@
from django import forms
from timer.models import Timer, Location
class TimerForm(forms.ModelForm):
location = forms.ModelChoiceField(Location.objects.none())
class Meta:
model = Timer

View File

@@ -3,6 +3,16 @@ from .utils import InheritanceQuerySet
from .owners import Corporation
class LocationManager(models.Manager):
use_for_related_fields = True
def get_query_set(self):
return InheritanceQuerySet(self.model)
def select_subclasses(self, *subclasses):
return self.get_query_set().select_subclasses(*subclasses)
def get_subclass(self, *args, **kwargs):
return self.get_query_set().select_subclasses().get(*args, **kwargs)
def all_subclassed(self):
return InheritanceQuerySet(model=self.model).select_subclasses()
@@ -15,8 +25,17 @@ class Location(models.Model):
y = models.BigIntegerField('Y Location', null=True)
z = models.BigIntegerField('Z Location', null=True)
objects = LocationManager()
def __unicode__(self):
return "%(name)s (%(id)d)" % self.__dict__
return "%(name)s" % self.__dict__
def get_subclass(self):
return Location.objects.get_subclass(pk=self.pk)
@property
def get_type(self):
return self.get_subclass().__class__.__name__
class Meta:
app_label = 'timer'
@@ -77,6 +96,10 @@ class Planet(Location):
class Moon(Location):
planet = models.ForeignKey(Planet, related_name='moons')
@property
def system(self):
return self.planet.system
class Meta:
app_label = 'timer'

View File

@@ -12,11 +12,11 @@ class Timer(models.Model):
(STATE_EXPIRED, 'Expired'),
)
TYPE_SHEILD_REENFORCEMENT = 1
TYPE_SHIELD_REENFORCEMENT = 1
TYPE_ARMOR_REENFORCEMENT = 2
TYPE_CHOICES = (
(TYPE_SHEILD_REENFORCEMENT, 'Sheild Reenforcement'),
(TYPE_SHIELD_REENFORCEMENT, 'Shield Reenforcement'),
(TYPE_ARMOR_REENFORCEMENT, 'Armor Reenforcement'),
)
@@ -30,6 +30,12 @@ class Timer(models.Model):
return self.STATE_EXPIRED
return self.STATE_ACTIVE
def get_state_display(self):
state = self.state
for v, disp in self.STATE_CHOICES:
if state == v:
return disp
class Meta:
app_label = 'timer'
ordering = ['-expiration']

View File

@@ -1,30 +1,91 @@
from django.db.models.fields.related import SingleRelatedObjectDescriptor
import django
from django.db.models.query import QuerySet
from django.db.models.fields.related import OneToOneField
from django.core.exceptions import ObjectDoesNotExist
try:
from django.db.models.constants import LOOKUP_SEP
except ImportError: # Django < 1.5
from django.db.models.sql.constants import LOOKUP_SEP
class InheritanceQuerySet(QuerySet):
def select_subclasses(self, *subclasses):
if not subclasses:
subclasses = [o for o in dir(self.model)
if isinstance(getattr(self.model, o), SingleRelatedObjectDescriptor) \
and issubclass(getattr(self.model, o).related.model, self.model)]
# only recurse one level on Django < 1.6 to avoid triggering
# https://code.djangoproject.com/ticket/16572
levels = None
if django.VERSION < (1, 6, 0):
levels = 1
subclasses = self._get_subclasses_recurse(self.model, levels=levels)
# workaround https://code.djangoproject.com/ticket/16855
field_dict = self.query.select_related
new_qs = self.select_related(*subclasses)
if isinstance(new_qs.query.select_related, dict) and isinstance(field_dict, dict):
new_qs.query.select_related.update(field_dict)
new_qs.subclasses = subclasses
return new_qs
def _clone(self, klass=None, setup=False, **kwargs):
try:
kwargs.update({'subclasses': self.subclasses})
except AttributeError:
pass
for name in ['subclasses', '_annotated']:
if hasattr(self, name):
kwargs[name] = getattr(self, name)
return super(InheritanceQuerySet, self)._clone(klass, setup, **kwargs)
def annotate(self, *args, **kwargs):
qset = super(InheritanceQuerySet, self).annotate(*args, **kwargs)
qset._annotated = [a.default_alias for a in args] + list(kwargs.keys())
return qset
def iterator(self):
iter = super(InheritanceQuerySet, self).iterator()
if getattr(self, 'subclasses', False):
for obj in iter:
obj = [getattr(obj, s) for s in self.subclasses if getattr(obj, s)] or [obj]
yield obj[0]
sub_obj = None
for s in self.subclasses:
sub_obj = self._get_sub_obj_recurse(obj, s)
if sub_obj:
break
if not sub_obj:
sub_obj = obj
if getattr(self, '_annotated', False):
for k in self._annotated:
setattr(sub_obj, k, getattr(obj, k))
yield sub_obj
else:
for obj in iter:
yield obj
yield obj
def _get_subclasses_recurse(self, model, levels=None):
rels = [rel for rel in model._meta.get_all_related_objects()
if isinstance(rel.field, OneToOneField)
and issubclass(rel.field.model, model)]
subclasses = []
if levels:
levels -= 1
for rel in rels:
if levels or levels is None:
for subclass in self._get_subclasses_recurse(
rel.field.model, levels=levels):
subclasses.append(rel.var_name + LOOKUP_SEP + subclass)
subclasses.append(rel.var_name)
return subclasses
def _get_sub_obj_recurse(self, obj, s):
rel, _, s = s.partition(LOOKUP_SEP)
try:
node = getattr(obj, rel)
except ObjectDoesNotExist:
return None
if s:
child = self._get_sub_obj_recurse(node, s)
return child or node
else:
return node

View File

@@ -0,0 +1,346 @@
(function ($) {
/* KEY constant copied from jquery autocomplete:
* http://bassistance.de/jquery-plugins/jquery-plugin-autocomplete/
*/
var KEY = {
UP: 38,
DOWN: 40,
DEL: 46,
TAB: 9,
ENTER: 13,
ESC: 27,
COMMA: 188,
PAGEUP: 33,
PAGEDOWN: 34,
BACKSPACE: 8
};
function InputDropdown($elem, options) {
this.$elem = $elem;
this.options = $.extend({
update_input: true,
no_results_html: 'Sorry, we couldn\'t find anything.',
ignore_history: true,
process_results: function (r) { return r; }
}, options);
var $field_with_icon = this.$elem.closest('.field_with_icon'),
$existing_results = $([]);
if ($field_with_icon.length) {
$existing_results = $field_with_icon.siblings('.results');
} else {
$existing_results = this.$elem.siblings('.results');
}
if ($existing_results.length) {
this.$results = $existing_results;
this.$results.find('div.no_results').remove();
} else {
this.$results = $('<div class="results"></div>');
if ($field_with_icon.length) {
$field_with_icon.after(this.$results);
} else {
this.$elem.after(this.$results);
}
}
if (!this.$results.find('ul.result_list').length) {
this.$results.append('<ul class="result_list"></ul>');
}
this.$no_results = $('<div class="no_results">' + this.options.no_results_html + '</div>');
this.$no_results.hide();
this.$results.append(this.$no_results);
this.handle_optional_content();
// css should do this
//this.$results.hide();
//this.$results.width(this.$elem.outerWidth());
this.livesearch = $elem.livesearch(this.options).data('livesearch');
this._attach();
}
$.fn.livesearch_input_dropdown = function (options) {
options = options || {};
return $(this).each(function () {
var input_dropdown = $(this).data('livesearch.input_dropdown');
if (!input_dropdown) {
input_dropdown = new InputDropdown($(this), options);
$(this).data('livesearch.input_dropdown', input_dropdown);
}
});
};
$.extend(InputDropdown.prototype, {
_attach: function () {
var _this = this;
this.$elem.bind('livesearch:results', function (e, results) {
if (_this.options.process_results) {
results = _this.options.process_results(results);
}
_this.show_results(results);
_this.push_history(results);
});
this.bind_results();
this.bind_popstate();
this.$elem.bind(($.browser.opera ? "keypress" : "keydown") + ".autocomplete", function (e) {
var something_selected = !!_this.$results.find('.selected').length,
$prev,
$next,
$selected;
switch (e.keyCode) {
case KEY.UP:
if (something_selected) {
$prev = _this.$results.find('.selected').prev(':not(.not_result)');
if (!$prev.length) {
$prev = _this.$results.find('li:not(.not_result)').last();
}
_this.select($prev, false);
} else {
_this.select(_this.$results.find('li:not(.not_result)').last(), false);
}
e.preventDefault();
break;
case KEY.DOWN:
if (something_selected) {
$next = _this.$results.find('.selected').next(':not(.not_result)');
if (!$next.length) {
$next = _this.$results.find('li:not(.not_result)').first();
}
_this.select($next, false);
} else {
_this.select(_this.$results.find('li:not(.not_result)').first(), false);
}
e.preventDefault();
break;
case KEY.ENTER:
// we want to trigger the selected event
$selected = _this.$results.find('.selected');
if (!$selected.length && _this.input_can_submit()) {
return;
}
_this.select($selected, true);
e.preventDefault();
break;
default:
break;
}
});
},
history_api_supported: function () {
return window.history && window.history.pushState;
},
bind_popstate: function () {
if (!this.history_api_supported() || this.options.ignore_history) { return; }
var _this = this;
this.replacing_history_state = false;
window.onpopstate = function (e) {
if (e.state && e.state.livesearch) {
// We've got a search object in history, so let's restore the input to that state.
_this.show_results(e.state.livesearch.results);
_this.livesearch.suspend_while(function () {
_this.$elem.val(e.state.livesearch.input_value).trigger('input');
});
} else {
// We're at a new or null history state. We may have got
// here via the back button from an executed search, or have landed here via http search.
// Let's ensure a blank input.
_this.reset(true);
}
};
},
input_can_submit: function () {
var
readyState = this.livesearch.search_xhr.readyState,
search_in_progress = (readyState > 0 && readyState < 4);
return this.options.input_can_submit_on_enter && !search_in_progress && this.$elem.is(":focus");
},
bind_results: function () {
var _this = this;
this.$results.find('li').bind('click', function () {
_this.select($(this), true);
});
this.$results.find('li').bind('mouseover', function () {
_this.select($(this));
_this.unselect_on_mouseout = true;
});
this.$results.bind('mouseout', function () {
if (_this.unselect_on_mouseout) {
_this.unselect_currently_selected();
}
});
this.$elem.bind('livesearch:reveal_results', function () {
_this.reveal_results();
});
},
/* Accepts an array of results, and returns an array of result
* names (strings)
*/
build_result_names_list: function (results) {
var result_names = [],
_this = this;
$.each(results, function () {
var name = this;
if (_this.options.return_name_from_result) {
name = _this.options.return_name_from_result(this);
} else if (this !== 'string') {
name = this[0];
}
result_names.push(name);
});
return result_names;
},
show_results: function (results) {
var $results_ul = this.$results.children('ul'),
$full_search_link = $([]),
result_names,
text,
arrow_icon,
meta;
if (results.results) {
meta = results;
results = results.results;
}
this.unselect_currently_selected();
$results_ul.empty();
if (!results.length) {
this.$no_results.show();
$results_ul.hide();
} else {
this.$no_results.hide();
$results_ul.show();
}
result_names = this.build_result_names_list(results);
$.each(result_names, function (index) {
var name = this,
$li = $('<li>' + name + '</li>');
$li.data('livesearch_result', results[index]);
$results_ul.append($li);
});
if (meta && meta.count > results.length) {
text = 'see all ' + meta.count + ' results';
arrow_icon = '<span class="icon-arrow"></span>&nbsp;';
$full_search_link = $('<a>').attr({
'href': this.livesearch.url + '?' + this.livesearch.last_search,
'title': text,
'class': 'see_all not_result'
}).html(arrow_icon + text).wrap('<li>');
$results_ul.append($full_search_link);
}
this.bind_results();
this.$elem.trigger('livesearch:reveal_results');
},
push_history: function (results) {
if (!this.history_api_supported() || this.options.ignore_history) { return; }
if (typeof results === 'string') {
results = JSON.parse(results);
}
var state = {
livesearch : {
input_value : this.$elem.val(),
results : results
}
};
if (this.replacing_history_state) {
window.history.replaceState(state);
this.replacing_history_state = false;
} else {
window.history.pushState(state, "", '?' + this.livesearch.last_search);
}
},
reveal_results: function () {
var _this = this;
this.$results.slideDown(function () {
$(window).resize();
_this.$results.trigger('sticky_bar:fix_to_bottom');
_this.$results.trigger('shifted');
});
},
reset: function (clear) {
this.$results.hide();
this.$elem.val('');
if (clear) {
this.$results.children('ul.result_list').html('');
}
},
unselect_currently_selected: function () {
var $results_ul = this.$results.children('ul'),
$last_selected = $results_ul.children('li.selected');
$last_selected.trigger('livesearch:unselect');
$last_selected.removeClass('selected');
// We're here because of a mouseout, or because the user selected something with
// the keyboard, or clicking. In either case, we don't want to unselect on the
// next mouseout.
this.unselect_on_mouseout = false;
},
// There are two kinds of selects:
// - Hard selects, these are triggered by clicks or the enter key. They
// trigger the select event.
// - Soft selects, these are triggered by arrowing up or down. They do not
// trigger the select event.
select: function ($li, trigger) {
var _this = this;
if ($li.is('.not_result')) { return; }
this.unselect_currently_selected();
$li.addClass('selected');
if (this.options.update_input) {
this.livesearch.suspend_while(function () {
_this.$elem.val($li.text());
_this.$elem.focus();
});
}
$li.trigger('livesearch:soft_select');
if (trigger) {
$li.trigger('livesearch:selected', [$li.data('livesearch_result')]);
}
},
handle_optional_content: function () {
if (this.options.prepend) {
this.$results.prepend(this.options.prepend);
}
}
});
}(jQuery));

View File

@@ -0,0 +1,148 @@
(function ($) {
//The timeout has to live in this scope
var timeout;
function LiveSearch($elem, options) {
this.$elem = $elem;
this.$form = $elem.closest('form');
this.options = $.extend({
delay: 400,
minimum_characters: 3,
serialize: this.$form,
client_side_cache: true,
process_data: false
}, options);
if (this.options.file_extension) {
this.ajax_url = this.ensure_file_extension(this.options.file_extension);
} else {
this.ajax_url = this.url;
}
this.last_search = false;
this.search_xhr = false;
if (this.options.client_side_cache) {
this.cache = {};
} else {
this.cache = false;
}
this.active = true;
this._attach();
}
$.fn.livesearch = function (options) {
options = options || {};
return $(this).each(function () {
var livesearch = $(this).data('livesearch');
if (!livesearch) {
livesearch = new LiveSearch($(this), options);
$(this).data('livesearch', livesearch);
}
});
};
$.extend(LiveSearch.prototype, {
_attach: function () {
var _this = this;
this.$elem.attr('autocomplete', 'off'); //we got this, yall
this.$elem.bind("keypress cut paste input", function () {
if (!_this.active) { return; }
clearTimeout(timeout);
timeout = setTimeout(function () {
_this.search();
}, _this.options.delay);
});
this.options.serialize.bind('change', function () {
_this.search();
});
this.$elem.bind('livesearch:suspend', function () {
_this.active = false;
});
this.$elem.bind('livesearch:activate', function () {
_this.active = true;
});
this.$elem.bind('livesearch:cancel', function () {
if (_this.search_xhr) {
_this.search_xhr.abort();
}
_this.last_search = false;
});
},
ensure_file_extension: function (extension) {
var
host_regexp_string = window.location.host.replace(/[^\w\d]/g, function (m) { return '\\' + m; }),
file_extension_regexp = new RegExp("((?:" + host_regexp_string + ")?[^\\.$#\\?]+)(\\.\\w*|)($|#|\\?)");
return this.url.replace(file_extension_regexp, function (m, _1, _2, _3) { return _1 + '.' + extension + _3; });
},
suspend_while: function (func) {
this.active = false;
func();
// TODO: this timeout is to to allow events to bubble before re-enabling,
// but I'm not sure why bubbling doesn't occur synchronously.
var _this = this;
setTimeout(function () {
_this.active = true;
}, 100);
},
search: function () {
var _this = this,
form_data = this.options.serialize.serialize();
if (this.options.process_data) {
form_data = this.options.process_data.apply(this, [form_data]);
if (typeof form_data === 'object') {
form_data = $.param(form_data);
}
}
if (form_data === this.last_search) { return; }
if (this.$elem.val().length < this.options.minimum_characters) { return; }
if (this.search_xhr) {
this.search_xhr.abort();
}
if (this.cache && this.cache[form_data] && typeof (this.cache[form_data]) !== 'function') {
this.$elem.trigger('livesearch:results', [this.cache[form_data]]);
} else {
this.$elem.trigger('livesearch:searching');
this.$elem.addClass('searching');
this.search_xhr = $.ajax({
type: 'get',
url: this.options.url || this.$form.attr('action'),
dataType: 'json',
data: form_data,
global: false,
success: function (data, textStatus, xhr) {
// this is the best workaround I can think of for
// http://dev.jquery.com/ticket/6173
if (data === null) { return; }
_this.$elem.trigger('livesearch:results', [data]);
_this.$elem.removeClass('searching');
if (_this.cache) {
_this.cache[form_data] = data;
}
},
error: function () {
_this.$elem.trigger('livesearch:ajax_error');
_this.$elem.removeClass('searching');
}
});
}
this.last_search = form_data;
}
});
}(jQuery));

View File

@@ -0,0 +1,100 @@
(function ($) {
$.fn.livesearch_multi_selector = function (options) {
options = $.extend({
url: false,
cancel_copy: 'Remove',
sortable: false,
update_input: false
}, options);
return $(this).each(function () {
var $div = $(this),
$input = $div.find('input[type="text"]'),
$field_with_icon = $input.closest('.field_with_icon'),
$list = $div.find('ol.search-selected,ul.search-selected'),
$search_loading_icon = $input.siblings('.icon-search'),
name = $input.attr('name'),
position_name = $input.siblings('.position').attr('name'),
this_options = options,
input_dropdown;
this_options.url = options.url || $div.closest('form').attr('action');
input_dropdown = $input.livesearch_input_dropdown(this_options);
if (options.sortable) {
$list.sortable({
containment: 'document',
axis: 'y',
update: function () {
$list.find('input.position').each(function (i) {
$(this).val(i);
});
}
});
}
function unselectable($li) {
var $cancel = $('<a class="cancel-link" href="#">' + options.cancel_copy + '</a>'),
$destroy = $li.find('.destroy');
$li.append($cancel);
if ($destroy.val() && $destroy.val() !== 'false') {
$li.hide();
}
$cancel.click(function (e) {
e.preventDefault();
$li.fadeOut(function () {
$destroy.val('1');
if (!$destroy.length) { //new record
$li.remove();
}
});
});
}
function reset() {
$input.val('');
$field_with_icon.siblings('.results').slideUp();
$input.trigger('livesearch:cancel');
}
$div.addClass('search');
$input.attr('name', '');
$list.find('li').each(function () {
unselectable($(this));
});
$input.bind('livesearch:searching', function () {
$search_loading_icon.removeClass('icon-search').addClass('icon-loading-small');
});
$input.bind('livesearch:results livesearch:ajax_error', function () {
$search_loading_icon.removeClass('icon-loading-small').addClass('icon-search');
});
$div.bind('livesearch:selected', function (e, data) {
if (!data) { return; }
var this_name = name.replace('[template]', '[' + $list.children('li').length + ']'),
this_position_name = false,
$li;
if (position_name) {
this_position_name = position_name.replace('[template]', '[' + $list.children('li').length + ']');
}
$li = $('<li>' + data[0] + '<input type="hidden" name="' + this_name + '" value="' + data[1] + '"/></li>');
if (options.sortable) {
if (this_position_name) {
$li.append('<input type="hidden" class="position" name="' + this_position_name + '" />');
}
$list.find('input.position').each(function (i) {
$(this).val(i);
});
}
$list.append($li);
unselectable($li);
reset();
});
});
};
}(jQuery));

View File

@@ -0,0 +1,52 @@
(function ($) {
/*
* Behavior:
*
* A user starts typing into an input, when they stop typing, a list of matching pages are
* displayed. The user can select an entry with the up and down arrow key and hit enter,
* or they can click a result. When a result is selected, the corresponding url is navigated
* to.
*
* Requirements:
*
* jquery.livesearch
* jquery.livesearch.input_dropdown
* jquery.livesearch.pretty_input_dropdown
*
* Expected markup:
*
* <form>
* <div>
* <span class='icon-search'></span>
* <span class='icon-search-clear'></span>
* <input></input>
* <div class='results'></div>
* </div>
* </form>
*
* Expected JSON response from the XHR:
*
* [
* ['Title of page', 'http://url/'],
* ['Title of another page', 'http://url2/']
* ]
*
*/
$.fn.livesearch_navigator = function (options) {
options = options || {};
return $(this).each(function () {
var $form = $(this);
$form.livesearch_pretty_input_dropdown();
$form.bind('livesearch:selected', function (e, data) {
if (data) {
window.location = data[1];
}
});
});
};
}(jQuery));

View File

@@ -0,0 +1,103 @@
(function ($) {
/*
* Behavior:
*
* A user starts typing into an input, when they stop typing, a list of matching pages are
* displayed. The user can select an entry with the up and down arrow key and hit enter,
* or they can click a result. When a result is selected, the corresponding url is navigated
* to.
*
* Requirements:
*
* jquery.livesearch
* jquery.livesearch.input_dropdown
*
* Expected markup:
*
* <form>
* <div>
* <span class='icon-search'></span>
* <span class='icon-search-clear'></span>
* <input></input>
* <div class='results'></div>
* </div>
* </form>
*
* Expected JSON response from the XHR:
*
* [
* ['Title of page', 'http://url/'],
* ['Title of another page', 'http://url2/']
* ]
*
*/
$.fn.livesearch_pretty_input_dropdown = function (options) {
options = options || {};
return $(this).each(function () {
var $form = $(this),
$text = $form.find('input[type=text]'),
$icon_search_clear = $form.find('.icon-search-clear'),
$search_loading_icon = $text.siblings('.icon-search'),
input_dropdown;
$text.livesearch_input_dropdown($.extend(options, {update_input : false}));
input_dropdown = $text.data('livesearch.input_dropdown');
function handle_close_button() {
if (this.value) {
$icon_search_clear.show();
} else {
$icon_search_clear.hide();
}
}
handle_close_button.call($text[0]);
$text.bind('keypress cut paste input livesearch:close_results blur', handle_close_button);
$text.bind('livesearch:searching', function () {
$search_loading_icon.removeClass('icon-search').addClass('icon-loading-small');
});
$text.bind('livesearch:results livesearch:ajax_error', function () {
$search_loading_icon.removeClass('icon-loading-small').addClass('icon-search');
});
$text.bind('livesearch:results', function () {
// if this option is set, assume we want the cursor to stay in the input after search is done
if (options.input_can_submit_on_enter) {
return;
}
var $results = $text.siblings('.results');
input_dropdown.select($results.find('li:first'));
});
function clear_results() {
$text.val('');
$text.trigger('livesearch:cancel').trigger('livesearch:close_results');
}
$icon_search_clear.bind('click', function () {
$text.focus();
clear_results();
});
$text.bind('livesearch:close_results', function () {
$text.siblings('.results').slideUp(function () {
$(window).resize();
$(this).trigger('sticky_bar:fix_to_bottom');
});
});
$text.bind('blur', function () {
if ($text.val().length < 3) {
clear_results();
}
});
});
};
}(jQuery));

View File

@@ -0,0 +1,69 @@
(function ($) {
$.fn.livesearch_selector = function (options) {
options = $.extend({url: false, cancel_copy: 'Cancel', target_input: false}, options);
return $(this).each(function () {
var $div = $(this),
$input = $div.find('input[type="text"]'),
$hidden_input = options.target_input || $div.find('input[type="hidden"]'),
$search_loading_icon = $input.siblings('.icon-search'),
input_dropdown;
$div.addClass('search');
function select() {
$input.hide();
$input.attr('disabled', 'disabled');
$input.siblings('.icon-search-clear, .icon-search').hide();
var $value_div = $('<div class="field-selected"><span class="value">' + $input.val() + '</span><a class="cancel-link" href="#">' + options.cancel_copy + '</a></div>');
$input.siblings('div.field-selected').remove();
$input.after($value_div);
$input.siblings('.results').slideUp();
$input.trigger('livesearch_selector:select', [ $input.val() ]);
$value_div.find('a.cancel-link').click(function (e) {
e.preventDefault();
$input.val('');
$value_div.remove();
$input.removeAttr('disabled');
$input.siblings('.icon-search-clear, .icon-search').show();
$input.show();
$input.focus();
$hidden_input.val('');
$input.trigger('dirty');
$input.trigger('livesearch_selector:unselect');
});
}
//if the page loads with values in the inputs, switch to selected state
if ($hidden_input.val() && $input.val()) {
select();
}
options.url = options.url || $div.closest('form').attr('action');
input_dropdown = $input.livesearch_input_dropdown(options);
$input.bind('livesearch:searching', function () {
$search_loading_icon.removeClass('icon-search').addClass('icon-loading-small');
});
$input.bind('livesearch:results livesearch:ajax_error', function () {
$search_loading_icon.removeClass('icon-loading-small').addClass('icon-search');
});
$input.bind('livesearch:results', function () {
var $results = $input.siblings('.results');
input_dropdown.select($results.find('li:first'));
});
$div.bind('livesearch:selected', function (e, data) {
if (!data) {
return;
}
$hidden_input.val(data[1]);
select();
});
});
};
}(jQuery));

View File

@@ -0,0 +1,25 @@
{% extends "base.html" %}
{% load staticfiles %}
{% block content %}
<div class="page-header">
<h1>Add Timer</h1>
</div>
<form method="post">
{{ form.as_p }}
{% csrf_token %}
<input class="btn btn-success" type="submit" value="Add Timer" />
</form>
{% endblock %}
{% block scripts %}
<script type="text/javascript" src="{% static "js/jquery.livesearch.js" %}"></script>
<script type="text/javascript">
$(function() {
$('id_location').livesearch();
$('form').bind('livesearch:results', function(e, results) {
console.log(results);
});
});
</script>
{% endblock %}

View File

@@ -1,3 +1,75 @@
{% for timer in timer_list %}
{{ timer.location.name }} - {{ timer.expiration_datetime }}
{% endfor %}
{% extends "base.html" %}
{% block content %}
<div class="row-fluid">
<div class="span9">
<p class="pull-left">
<a class="badge {% if not list_type or list_type == 0 %}badge-info{% endif %}" href="?{% if list_all %}all={{ list_all }}&{% endif %}type=0">All</a>
<a class="badge {% if list_type == 1 %}badge-info{% endif %}" href="?{% if list_all %}all={{ list_all }}&{% endif %}type=1">Stations</a>
<a class="badge {% if list_type == 2 %}badge-info{% endif %}" href="?{% if list_all %}all={{ list_all }}&{% endif %}type=2">IHub</a>
<a class="badge {% if list_type == 3 %}badge-info{% endif %}" href="?{% if list_all %}all={{ list_all }}&{% endif %}type=3">POS</a>
</p>
</div>
<div class="span3">
<p class="pull-right">
<a class="badge {% if not list_all %}badge-info{% endif %}" href="?{% if list_type %}type={{ list_type }}&{% endif %}all=0">Active</a>
<a class="badge {% if list_all %}badge-info{% endif %}" href="?{% if list_type %}type={{ list_type }}&{% endif %}all=1">All</a>
</p>
</div>
</div>
<div class="row-fluid">
<table class="table">
<thead>
<tr><th>Location</th><th>Location Type</th><th>System</th><th>Type</th><th>Expiration</th><th>Status</th><th>Actions</th></tr>
</thead>
<tbody>
{% for timer in object_list %}
<tr>
<td>{{ timer.location.name }}</td>
<td>{{ timer.location.get_type }}</td>
<td><a href="http://evemaps.dotlan.net/system/{{ timer.location.get_subclass.system }}">{{ timer.location.get_subclass.system }}</a></td>
<td>{{ timer.get_reenforcement_type_display }}</td>
<td class="time" data-unixtime="{{ timer.expiration|date:"U" }}">{{ timer.expiration }}</td>
<td>{{ timer.get_state_display }}</td>
<td></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
{% block scripts %}
<script type="text/javascript">
function secondsToTime(secs)
{
var hours = Math.floor(secs / (60 * 60));
var divisor_for_minutes = secs % (60 * 60);
var minutes = Math.floor(divisor_for_minutes / 60);
var divisor_for_seconds = divisor_for_minutes % 60;
var seconds = Math.ceil(divisor_for_seconds);
var obj = {
"h": hours,
"m": minutes,
"s": seconds
};
return obj;
}
function update_timers() {
$('td.time').each(
function (e) {
if (this.attributes['data-unixtime']) {
var now = new Date();
var secs = this.attributes['data-unixtime'].value - Math.round((now.getTime() / 1000));
if (secs > 0) {
var res = secondsToTime();
this.innerHTML = res.h + "h " + res.m + "m " + res.s + "s";
}
}
}
);
}
setInterval(update_timers, 1000);
update_timers();
</script>
{% endblock %}

View File

@@ -1,6 +1,8 @@
from django.conf.urls import patterns, url
from timer.views import TimerListView
from timer.views import TimerListView, TimerCreateView
urlpatterns = patterns('',
url(r'^$', TimerListView.as_view(), name='timer-list'),
url(r'^create/$', TimerCreateView.as_view(), name='timer-create')
)

View File

@@ -1,14 +1,38 @@
from django.views.generic import ListView
from django.views.generic import ListView, CreateView
from django.utils.timezone import now
from timer.models import Timer
from timer.models import Timer, Location, Station, Moon, System
from timer.forms import TimerForm
class TimerCreateView(CreateView):
model = Timer
form_class = TimerForm
class TimerListView(ListView):
model = Timer
template_name = 'timer/timer_list.html'
def get_queryset(self):
qs = super(TimerListView, self).get_queryset()
if 'active' in self.kwargs:
qs = qs.filter(expiry_datetime__gt=now())
qs = super(TimerListView, self).get_queryset().select_related('location', 'station', 'moon', 'system')
if int(self.request.GET.get('all', 0)) == 0:
qs = qs.filter(expiration__gt=now())
type = int(self.request.GET.get('type', 0))
if type == 1:
qs = [m for m in qs if m.location.get_type == 'Station']
if type == 2:
qs = [m for m in qs if m.location.get_type == 'System']
if type == 3:
qs = [m for m in qs if m.location.get_type == 'Moon']
return qs
def get_context_data(self, **kwargs):
ctx = super(TimerListView, self).get_context_data(**kwargs)
ctx.update({
'list_all': int(self.request.GET.get('all', 0)),
'list_type': int(self.request.GET.get('type', 0)),
})
return ctx