diff --git a/src/app.yaml b/src/app.yaml index c39b4b2..6a3bc0e 100644 --- a/src/app.yaml +++ b/src/app.yaml @@ -9,6 +9,8 @@ libraries: version: latest - name: markupsafe version: latest +- name: numpy + version: latest derived_file_type: - python_precompiled diff --git a/src/index.yaml b/src/index.yaml index a760132..dc97c04 100644 --- a/src/index.yaml +++ b/src/index.yaml @@ -15,3 +15,18 @@ indexes: - name: character_key - name: td direction: desc + +- kind: WalletTransaction + properties: + - name: character_key + - name: tt + - name: td + direction: desc + +- kind: WalletTransaction + properties: + - name: character_key + - name: tt + - name: tyi + - name: td + direction: desc diff --git a/src/maeve/character_view.py b/src/maeve/character_view.py index 1c38a51..1a7c53c 100644 --- a/src/maeve/character_view.py +++ b/src/maeve/character_view.py @@ -3,7 +3,7 @@ from maeve.web import BaseHandler, profile_required from maeve.settings import webapp2_config from maeve.utils import is_prod_environment -from maeve.models import Account, Character +from maeve.models import Account, Character, WalletTransaction from google.appengine.ext.ndb import toplevel from google.appengine.api import users import webapp2 @@ -18,7 +18,8 @@ def get(self, char_id): if char: env.update(dict(character=char, - account=char.account)) + account=char.account, + current='dashboard')) if char.active: pass @@ -47,9 +48,30 @@ def post(self, char_id, action): self.session.add_flash('No character with that id found', key='top_messages', level='warning') self.redirect('/profile') + +class CharacterTransactionsHandler(BaseHandler): + + def get(self, char_id): + env = {} + char = Character.by_char_id(char_id) + if char: + env.update(dict(character=char, + account=char.account, + current='transactions', + WalletTransaction=WalletTransaction)) + + if char.active: + pass + + self.render_response('character/transactions.html', env) + else: + self.session.add_flash('No character with that id found', key='top_messages') + self.redirect('/profile') + app = webapp2.WSGIApplication([ (r'/character/(\d+)/?$', CharacterHandler), (r'/character/(\d+)/(activate)/?$', CharacterActivationHandler), + (r'/character/(\d+)/transactions/?$', CharacterTransactionsHandler), ], debug=(not is_prod_environment()), config=webapp2_config diff --git a/src/maeve/models.py b/src/maeve/models.py index 3f0b55b..ae97a3d 100644 --- a/src/maeve/models.py +++ b/src/maeve/models.py @@ -140,4 +140,24 @@ class ItemTypeIndex(model.Model): @classmethod def get(cls, item_id): idx = ItemTypeIndex.query().get() - return idx.items or {} + return (idx.items or {}).get(item_id, None) + + def find(cls, name_part): + import re + name_re = re.compile('^.*?{0}.*$'.format(name_part.lower()), flags=re.IGNORECASE) + idx = ItemTypeIndex.query().get() + matches = ([(i, n) for i, n in idx.items.iteritems() if name_re.match(n)]) + return matches + + +class ItemStats(model.Model): + user = model.UserProperty('u', required=True) + character_key = model.KeyProperty('ck', kind='Character', required=True) + char_id = model.StringProperty('cid', required=True) + type_id = model.StringProperty('iid', required=True) + accumulated_cost = model.FloatProperty('ac', default=0, indexed=False) + accumulated_earnings = model.FloatProperty('ae', default=0, indexed=False) + items_sold = model.IntegerProperty('is', default=0) + items_bought = model.IntegerProperty('ib', default=0) + roi_yield = model.FloatProperty('ry', default=0) + avg_roi_yield = model.FloatProperty('ary', default=0) diff --git a/src/maeve/statistics.py b/src/maeve/statistics.py index ca42bea..70927a6 100644 --- a/src/maeve/statistics.py +++ b/src/maeve/statistics.py @@ -1,16 +1,24 @@ # -*- coding: UTF-8 -*- -from maeve.web import BaseHandler, profile_required -from maeve.settings import webapp2_config -from maeve.utils import is_prod_environment from maeve.models import Character, WalletTransaction -from google.appengine.ext.ndb import toplevel -from google.appengine.api import users +from maeve.utils import price_fmt from datetime import datetime +import logging + + +def extend_transactions_query_result(result): + results_as_dct = [r.to_dict() for r in result] + for dct in results_as_dct: + dct['balance_change'] = dct['unit_price'] * dct['quantity'] + if dct['transaction_type'] == WalletTransaction.BUY: + dct['balance_change'] *= -1 + + dct['balance_change_str'] = price_fmt(dct['balance_change']) + dct['unit_price_str'] = price_fmt(dct['unit_price']) def get_filtered_transactions(character, filters): - transaction_type = filters.get('type', None) + transaction_type = filters.get('transaction_type', None) if 'limit' in filters: limit = filters['limit'] else: @@ -22,6 +30,12 @@ def get_filtered_transactions(character, filters): query = WalletTransaction.query(WalletTransaction.character_key == character.key) if transaction_type: + if type(transaction_type) in (str, unicode): + if transaction_type.lower() == 'sell': + transaction_type = WalletTransaction.SELL + elif transaction_type.lower() == 'buy': + transaction_type = WalletTransaction.BUY + query = query.filter(WalletTransaction.transaction_type == int(transaction_type)) if 'type_id' in filters: diff --git a/src/maeve/statistics_view.py b/src/maeve/statistics_view.py index 2241957..4cd9e20 100644 --- a/src/maeve/statistics_view.py +++ b/src/maeve/statistics_view.py @@ -2,13 +2,14 @@ from maeve.web import BaseHandler, profile_required from maeve.settings import webapp2_config -from maeve.utils import is_prod_environment, GenericModelEncoder, price_fmt -from maeve.models import Character, WalletTransaction -from maeve.statistics import get_filtered_transactions +from maeve.utils import is_prod_environment, GenericModelEncoder, price_fmt, to_jstime +from maeve.models import Character, WalletTransaction, ItemTypeIndex +from maeve.statistics import get_filtered_transactions, extend_transactions_query_result from google.appengine.ext.ndb import toplevel from google.appengine.api import users import webapp2 import json +import numpy class TransactionsHandler(BaseHandler): @@ -30,9 +31,56 @@ def get(self): self.render_json(results_as_dct, cls=GenericModelEncoder) +class TransactionsAverageHandler(BaseHandler): + + def get(self): + try: + char_id = self.request.get('char', None) + type_id = self.request.get('type_id', None) + quantity = int(self.request.get('quantity', 10)) + transaction_type = int(self.request.get('transaction_type', WalletTransaction.BUY)) + + result = get_filtered_transactions(Character.by_char_id(char_id), + filters=dict(transaction_type=transaction_type, + type_id=type_id, + limit=quantity)) + + oldest_date, prices, i = None, [], 0 + for t in result: + if i > quantity: + break + + oldest_date = t.transaction_date + for j in range(t.quantity): + prices.append(t.unit_price) + i += 1 + if i > quantity: + break + + self.render_json(dict(prices=prices, + oldest_date=to_jstime(oldest_date), + median=numpy.median(prices), + mean=numpy.mean(prices))) + + except: + import traceback + import logging + logging.error(traceback.format_exc()) + self.render_json(dict(error='Bad values')) + + +class SeachCommodityHandler(BaseHandler): + + def get(self): + query = self.request.get('query', '') + index = ItemTypeIndex.query().get() + + self.render_json(dict(matches=index.find(query))) app = webapp2.WSGIApplication([ - (r'/stat/transactions?$', TransactionsHandler), + (r'/stat/commodity/search/?$', SeachCommodityHandler), + (r'/stat/transactions/?$', TransactionsHandler), + (r'/stat/transactions/average/?$', TransactionsAverageHandler), ], debug=(not is_prod_environment()), config=webapp2_config diff --git a/src/maeve/task_view.py b/src/maeve/task_view.py index b7e024a..cac9a41 100644 --- a/src/maeve/task_view.py +++ b/src/maeve/task_view.py @@ -31,9 +31,10 @@ def post(self): if character and character.active: account = character.account_key.get() items = index_character(character, account) - taskqueue.add(url='/_task/index', - params={'values': json.dumps(items)}, - queue_name='index-update') + if items: + taskqueue.add(url='/_task/index', + params={'values': json.dumps(items)}, + queue_name='index-update') class IndexTaskHandler(BaseHandler): diff --git a/src/maeve/tasks.py b/src/maeve/tasks.py index 4f166ea..419acf8 100644 --- a/src/maeve/tasks.py +++ b/src/maeve/tasks.py @@ -1,7 +1,7 @@ # -*- coding: UTF-8 -*- from maeve.api import Api -from maeve.models import Character, Account, WalletTransaction, MarketOrder, ItemTypeIndex +from maeve.models import Character, Account, WalletTransaction, MarketOrder, ItemTypeIndex, ItemStats from google.appengine.ext import ndb from datetime import datetime from google.appengine.api import taskqueue @@ -49,58 +49,75 @@ def index_all_characters(): task_count += 1 taskqueue.add(url='/_task/sync', params={'char': character.key.urlsafe()}, - queue_name='transaction-sync') + queue_name='transaction-sync', + ) logging.info('{0} sync tasks enqueued'.format(task_count)) def index_character(character, account): - logging.info('Synching: Character {0} / {1}'.format(character.name, character.char_id)) - orders = MarketOrder.query(MarketOrder.character_key == character.key).fetch_async() + try: + logging.info('Synching: Character {0} / {1}'.format(character.name, character.char_id)) + item_stats = ItemStats.query(ItemStats.character_key == character.key).fetch_async() + orders = MarketOrder.query(MarketOrder.character_key == character.key).fetch_async() - api = Api(account.api_id, account.api_vcode) - api.authenticate() - api_char = api.get_character(character.char_id) + api = Api(account.api_id, account.api_vcode) + api.authenticate() + api_char = api.get_character(character.char_id) - row_count = 250 - all_items = {} + row_count = 250 + all_items = {} - character.last_update = datetime.now() + character.last_update = datetime.now() - last_transaction_id = character.last_transaction_id - last_transaction_date = character.last_transaction_date + last_transaction_id = character.last_transaction_id + last_transaction_date = character.last_transaction_date - api_wallet_transactions = api_char.WalletTransactions(rowCount=(last_transaction_id is None and 1000 or row_count)) - newest_transaction, oldest_transaction, items = sync_transactions(character, - api_wallet_transactions, - last_transaction_id, - last_transaction_date) + api_wallet_transactions = api_char.WalletTransactions(rowCount=(last_transaction_id is None and 1000 or row_count)) + item_stats = dict([(i.type_id, i) for i in item_stats.get_result()]) - all_items.update(items or {}) - - while last_transaction_id and last_transaction_date and oldest_transaction and \ - (datetime.fromtimestamp(oldest_transaction.transactionDateTime) > last_transaction_date or oldest_transaction.transactionID > last_transaction_id): - logging.info('Fetching another batch from id {0}'.format(oldest_transaction.transactionID)) - - api_wallet_transactions = api_char.WalletTransactions(rowCount=row_count, fromID=oldest_transaction.transactionID) newest_transaction, oldest_transaction, items = sync_transactions(character, api_wallet_transactions, last_transaction_id, - last_transaction_date) + last_transaction_date, + item_stats) all_items.update(items or {}) - sync_orders(character, - api_char.MarketOrders(), - orders.get_result()) + while last_transaction_id and last_transaction_date and oldest_transaction and \ + (datetime.fromtimestamp(oldest_transaction.transactionDateTime) > last_transaction_date or oldest_transaction.transactionID > last_transaction_id): + logging.info('Fetching another batch from id {0}'.format(oldest_transaction.transactionID)) + + api_wallet_transactions = api_char.WalletTransactions(rowCount=row_count, fromID=oldest_transaction.transactionID) + newest_transaction, oldest_transaction, items = sync_transactions(character, + api_wallet_transactions, + last_transaction_id, + last_transaction_date, + item_stats) + + all_items.update(items or {}) - character.put_async() - logging.info('Syncing done: Character {0} / {1}'.format(character.name, character.char_id)) - return all_items + sync_orders(character, + api_char.MarketOrders(), + orders.get_result()) + character.put_async() + logging.info('Syncing done: Character {0} / {1}'.format(character.name, character.char_id)) + return all_items + except: + import traceback + logging.error('Error while syncing character {0} / {1}'.format(character.name, character.char_id)) + logging.error(traceback.format_exc()) + return None + + +def sync_transactions(character, + api_wallet_transactions, + last_transaction_id, + last_transaction_date, + item_stats): -def sync_transactions(character, api_wallet_transactions, last_transaction_id, last_transaction_date): newest_transaction, oldest_transaction, items = None, None, {} to_put = [] @@ -123,6 +140,29 @@ def sync_transactions(character, api_wallet_transactions, last_transaction_id, l to_put.append(wt) items[wt.type_id] = wt.type_name + + stats = item_stats.get(wt.type_id, None) + abs_balance_change = wt.quantity * wt.unit_price + + if not stats: + stats = ItemStats(user=character.user, + char_id=character.char_id, + character_key=character.key, + type_id=wt.type_id, + accumulated_cost=(wt.transaction_type == WalletTransaction.BUY and abs_balance_change or 0), + accumulated_earnings=(wt.transaction_type == WalletTransaction.SELL and abs_balance_change or 0), + items_bought=(wt.transaction_type == WalletTransaction.BUY and wt.quantity or 0), + items_sold=(wt.transaction_type == WalletTransaction.SELL and wt.quantity or 0), + ) + item_stats[wt.type_id] = stats + else: + if wt.transaction_type == WalletTransaction.BUY: + stats.accumulated_cost += abs_balance_change + stats.items_bought += wt.quantity + else: + stats.accumulated_earnings += abs_balance_change + stats.items_sold += wt.quantity + else: logging.debug('Skipped transaction {0}'.format(row.transactionID)) @@ -138,6 +178,14 @@ def sync_transactions(character, api_wallet_transactions, last_transaction_id, l ndb.put_multi_async(to_put) + for stats in item_stats.values(): + stats.roi_yield = stats.accumulated_cost > 0 and (stats.accumulated_earnings - stats.accumulated_cost) / stats.accumulated_cost or 0 + avg_unit_cost = stats.items_bought > 0 and stats.accumulated_cost / stats.items_bought or 0 + avg_unit_earnings = stats.items_sold > 0 and stats.accumulated_earnings / stats.items_sold or 0 + stats.avg_roi_yield = avg_unit_cost > 0 and (avg_unit_earnings - avg_unit_cost) / avg_unit_cost or 0 + + ndb.put_multi_async(item_stats.values()) + return newest_transaction, oldest_transaction, items diff --git a/src/queue.yaml b/src/queue.yaml index d23815b..6cc605b 100644 --- a/src/queue.yaml +++ b/src/queue.yaml @@ -2,6 +2,8 @@ queue: - name: transaction-sync rate: 5/s + retry_parameters: + task_retry_limit: 1 - name: index-update rate: 1/s diff --git a/src/static/css/character.css b/src/static/css/character.css index 46f2cf6..12e6e93 100644 --- a/src/static/css/character.css +++ b/src/static/css/character.css @@ -34,3 +34,9 @@ .table-transactions > tbody > tr > td.currency { text-align: right; } + +.progress-container { + width: 50%; + position: relative; + left: 25%; +} diff --git a/src/static/css/main.css b/src/static/css/main.css index ca5b393..ea7134d 100644 --- a/src/static/css/main.css +++ b/src/static/css/main.css @@ -10,3 +10,11 @@ form h2, form h3 { margin-bottom: 10px; } + +li, p { + font-size: 1.2em; +} + +.well > h2 { + margin-bottom: 8px; +} diff --git a/src/static/js/character.js b/src/static/js/character.js index a1c6126..08b3acc 100644 --- a/src/static/js/character.js +++ b/src/static/js/character.js @@ -2,32 +2,97 @@ mv.character = (function() { return new function() { - var self = this; + var self = this, + dom = {}, + callbacks = {}; this.init = function() { - self.updateTransactions(); + mv.character.items = {}; }; - this.updateTransactions = function() { - var ajaxOpts = { - url: '/stat/transactions', - type: 'GET', - data: { + this.initDashboard = function() { + self.updateTransactions($('#transactions > .content')); + }; + + this.initTransactions = function() { + dom.calculator = $('#calculator'); + dom.averageContainer = dom.calculator.find('#average-container'); + dom.averageForm = dom.calculator.find('#form-average'); + dom.averageFormSubmitButton = dom.averageForm.find('button'); + dom.averageFormNameInput = dom.averageForm.find('[name=commodity_name]'); + + self.updateTransactions($('.sell-transactions'), { 'transaction_type': 'sell' }); + self.updateTransactions($('.buy-transactions'), { 'transaction_type': 'buy' }); + + dom.averageFormSubmitButton.attr('disabled', true); + + dom.averageForm.on('submit', callbacks.averageFormSubmit); + + dom.averageFormNameInput.on('change', callbacks.commodityNameChange).typeahead({ + source: self.typeaheadCommodityName + }); + + }; + + this.updateTransactions = function($target, filterData) { + var data = { 'char': mv.data.character.id }, - dataType: 'JSON' - }; + ajaxOpts = { + url: '/stat/transactions', + type: 'GET', + data: data, + dataType: 'JSON' + }; + + if(filterData) { + data.filters = JSON.stringify(filterData); + } jQuery.ajax(ajaxOpts).done(function(jsonData) { var view = { transactions: jsonData }, - $output = $($.mustache(mv.templates.transactionTable, view)), - $target = $('#transactions'); + $output = $($.mustache(mv.templates.transactionTable, view)); $target.empty().append($output); }); }; + + this.typeaheadCommodityName = function(query, process) { + $.ajax({ + url: '/stat/commodity/search', + type: 'GET', + data: { + query: query + }, + dataType: 'JSON' + }).done(function(response) { + var source = []; + _.each(response.matches, function(c) { + source.push(c[1]); + mv.character.items[c[1]] = c[0]; + }) + process(source); + }) + }; + + callbacks.commodityNameChange = function(e) { + var $input = $(this), + commodity = mv.character.items[$input.val()]; + + dom.averageFormSubmitButton.attr('disabled', !(commodity)); + }; + + callbacks.averageFormSubmit = function(e) { + var commodity = mv.character.items[dom.averageFormNameInput.val()]; + + if(commodity) { + dom.averageContainer.find('.progress').show(); + } + e.preventDefault(); + return false; + }; } })(); diff --git a/src/static/js/lib/bootstrap-typeahead.js b/src/static/js/lib/bootstrap-typeahead.js new file mode 100644 index 0000000..6e896bb --- /dev/null +++ b/src/static/js/lib/bootstrap-typeahead.js @@ -0,0 +1,299 @@ +/* ============================================================= + * bootstrap-typeahead.js v2.0.4 + * http://twitter.github.com/bootstrap/javascript.html#typeahead + * ============================================================= + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + + +!function($){ + + "use strict"; // jshint ;_; + + + /* TYPEAHEAD PUBLIC CLASS DEFINITION + * ================================= */ + + var Typeahead = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, $.fn.typeahead.defaults, options) + this.matcher = this.options.matcher || this.matcher + this.sorter = this.options.sorter || this.sorter + this.highlighter = this.options.highlighter || this.highlighter + this.updater = this.options.updater || this.updater + this.$menu = $(this.options.menu).appendTo('body') + this.source = this.options.source + this.shown = false + this.listen() + } + + Typeahead.prototype = { + + constructor: Typeahead + + , select: function () { + var val = this.$menu.find('.active').attr('data-value') + this.$element + .val(this.updater(val)) + .change() + return this.hide() + } + + , updater: function (item) { + return item + } + + , show: function () { + var pos = $.extend({}, this.$element.offset(), { + height: this.$element[0].offsetHeight + }) + + this.$menu.css({ + top: pos.top + pos.height + , left: pos.left + }) + + this.$menu.show() + this.shown = true + return this + } + + , hide: function () { + this.$menu.hide() + this.shown = false + return this + } + + , lookup: function (event) { + var items + + this.query = this.$element.val() + + if (!this.query) { + return this.shown ? this.hide() : this + } + + items = $.isFunction(this.source) ? this.source(this.query, $.proxy(this.process, this)) : this.source + + return items ? this.process(items) : this + } + + , process: function (items) { + var that = this + + items = $.grep(items, function (item) { + return that.matcher(item) + }) + + items = this.sorter(items) + + if (!items.length) { + return this.shown ? this.hide() : this + } + + return this.render(items.slice(0, this.options.items)).show() + } + + , matcher: function (item) { + return ~item.toLowerCase().indexOf(this.query.toLowerCase()) + } + + , sorter: function (items) { + var beginswith = [] + , caseSensitive = [] + , caseInsensitive = [] + , item + + while (item = items.shift()) { + if (!item.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item) + else if (~item.indexOf(this.query)) caseSensitive.push(item) + else caseInsensitive.push(item) + } + + return beginswith.concat(caseSensitive, caseInsensitive) + } + + , highlighter: function (item) { + var query = this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&') + return item.replace(new RegExp('(' + query + ')', 'ig'), function ($1, match) { + return '' + match + '' + }) + } + + , render: function (items) { + var that = this + + items = $(items).map(function (i, item) { + i = $(that.options.item).attr('data-value', item) + i.find('a').html(that.highlighter(item)) + return i[0] + }) + + items.first().addClass('active') + this.$menu.html(items) + return this + } + + , next: function (event) { + var active = this.$menu.find('.active').removeClass('active') + , next = active.next() + + if (!next.length) { + next = $(this.$menu.find('li')[0]) + } + + next.addClass('active') + } + + , prev: function (event) { + var active = this.$menu.find('.active').removeClass('active') + , prev = active.prev() + + if (!prev.length) { + prev = this.$menu.find('li').last() + } + + prev.addClass('active') + } + + , listen: function () { + this.$element + .on('blur', $.proxy(this.blur, this)) + .on('keypress', $.proxy(this.keypress, this)) + .on('keyup', $.proxy(this.keyup, this)) + + if ($.browser.webkit || $.browser.msie) { + this.$element.on('keydown', $.proxy(this.keydown, this)) + } + + this.$menu + .on('click', $.proxy(this.click, this)) + .on('mouseenter', 'li', $.proxy(this.mouseenter, this)) + } + + , move: function (e) { + if (!this.shown) return + + switch(e.keyCode) { + case 9: // tab + case 13: // enter + case 27: // escape + e.preventDefault() + break + + case 38: // up arrow + e.preventDefault() + this.prev() + break + + case 40: // down arrow + e.preventDefault() + this.next() + break + } + + e.stopPropagation() + } + + , keydown: function (e) { + this.suppressKeyPressRepeat = !~[40,38,9,13,27].indexOf(e.keyCode) + this.move(e) + } + + , keypress: function (e) { + if (this.suppressKeyPressRepeat) return + this.move(e) + } + + , keyup: function (e) { + switch(e.keyCode) { + case 40: // down arrow + case 38: // up arrow + break + + case 9: // tab + case 13: // enter + if (!this.shown) return + this.select() + break + + case 27: // escape + if (!this.shown) return + this.hide() + break + + default: + this.lookup() + } + + e.stopPropagation() + e.preventDefault() + } + + , blur: function (e) { + var that = this + setTimeout(function () { that.hide() }, 150) + } + + , click: function (e) { + e.stopPropagation() + e.preventDefault() + this.select() + } + + , mouseenter: function (e) { + this.$menu.find('.active').removeClass('active') + $(e.currentTarget).addClass('active') + } + + } + + + /* TYPEAHEAD PLUGIN DEFINITION + * =========================== */ + + $.fn.typeahead = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('typeahead') + , options = typeof option == 'object' && option + if (!data) $this.data('typeahead', (data = new Typeahead(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.typeahead.defaults = { + source: [] + , items: 8 + , menu: '' + , item: '
  • ' + } + + $.fn.typeahead.Constructor = Typeahead + + + /* TYPEAHEAD DATA-API + * ================== */ + + $(function () { + $('body').on('focus.typeahead.data-api', '[data-provide="typeahead"]', function (e) { + var $this = $(this) + if ($this.data('typeahead')) return + e.preventDefault() + $this.typeahead($this.data()) + }) + }) + +}(window.jQuery); diff --git a/src/templates/character/base.html b/src/templates/character/base.html new file mode 100644 index 0000000..c9fde80 --- /dev/null +++ b/src/templates/character/base.html @@ -0,0 +1,74 @@ +{% extends 'base.html' %} + +{% block csshead %} + +{% endblock %} + +{% block bodycontent %} +
    +
    +
    + +
    +

    {{ character.name }}

    + Last updated: {% if character.last_update %}{{ character.last_update|datetime_fmt }}{% else %} Never {% endif %} +
    +
    +
    +
    +{% include 'top_messages.html' %} +
    + {% if not character.active %} +
    +
    +

    + This character is not active. Activate it now? +

    +
    + +
    +
    +
    + {% endif %} +
    + +{% if character.active %} +
    +
    + +
    +
    + {% block rightcontent %}{% endblock %} +
    +
    +{% endif %} +{% endblock %} + +{% block endbody %} + +{% if debug %} + +{% else %} + +{% endif %} + + +{% block subendbody %} +{% endblock %} +{% endblock %} diff --git a/src/templates/character/index.html b/src/templates/character/index.html index f7a37a8..cc3310c 100644 --- a/src/templates/character/index.html +++ b/src/templates/character/index.html @@ -1,57 +1,19 @@ -{% extends 'base.html' %} +{% extends 'character/base.html' %} -{% block csshead %} - -{% endblock %} - -{% block bodycontent %} -
    -
    -
    - -
    -

    {{ character.name }}

    - Last updated: {% if character.last_update %}{{ character.last_update|datetime_fmt }}{% else %} Never {% endif %} -
    -
    -
    -
    -{% include 'top_messages.html' %} -
    - {% if not character.active %} -
    -
    -

    - This character is not active. Activate it now? -

    -
    - -
    -
    -
    - {% endif %} -
    - -{% if character.active %} -
    -
    -
    -
    -
    +{% block rightcontent %} +

    Latest transactions

    +
    -{% endif %} {% endblock %} -{% block endbody %} - +{% endblock %} +{% block templates %} {% raw %} - mv.templates = {} mv.templates.transactionTable = ' \ \ \ @@ -76,12 +38,4 @@

    {{ character.name }}

    \
    '; {% endraw %} - -{% if debug %} - -{% else %} - -{% endif %} - - {% endblock %} diff --git a/src/templates/character/transactions.html b/src/templates/character/transactions.html new file mode 100644 index 0000000..e6004fd --- /dev/null +++ b/src/templates/character/transactions.html @@ -0,0 +1,74 @@ +{% extends 'character/base.html' %} + +{% block rightcontent %} +
    + +
    +
    +

    Latest sell transactions

    +
    +

    Latest buy transactions

    +
    +
    +
    +
    +

    History average

    +
    + + + + +
    +
    + +
    +
    +
    +
    +
    +
    + +{% endblock %} + +{% block subendbody %} + + +{% endblock %} + +{% block templates %} + {% raw %} + mv.templates.transactionTable = ' \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + {{#transactions}} \ + \ + \ + \ + \ + \ + \ + \ + {{/transactions}} \ + \ +
    CommodityDateQuantityUnit PriceBalance Change
    {{ type_name }}{{ transaction_date.str }}{{ quantity }}{{ unit_price_str }}{{ balance_change_str }}
    '; + {% endraw %} +{% endblock %}